|
73681
|
NULL
|
0
|
2026-04-23T08:17:20.213059+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776932240213_m1.jpg...
|
iTerm2
|
docker
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
root@docker_lamp_1:/home/jiminny# php artisan jimi root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-21 13:25:21
From: 2026-04-20 00:00:00
To: 2026-04-20 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-20 13:25:31
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-19 13:26:02
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-18 13:26:53
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-17 13:27:05
From: 2026-04-16 00:00:00
To: 2026-04-16 23:59:59
root@docker_lamp_1:/home/jiminny#
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66
Learn more at [URL_WITH_CREDENTIALS] ~ $ dev
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Count: 1
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Count: 1
All count: 1
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 4.21ms DONE
cache [PASSWORD_DOTS] 24.90ms DONE
compiled [PASSWORD_DOTS] 2.45ms DONE
events [PASSWORD_DOTS] 1.93ms DONE
routes [PASSWORD_DOTS] 2.01ms DONE
views [PASSWORD_DOTS] 9.92ms DONE
worker-download:worker-download_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-nudges:worker-nudges_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Count: 6
All count: 6
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
docker
Close Tab
-zsh
Close Tab
✳ Build full day activity summary from Screenpipe (claude)
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-159-186:~ (-zsh)
Close Tab
⌥⌘1
docker...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-21 13:25:21\nFrom: 2026-04-20 00:00:00\nTo: 2026-04-20 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-20 13:25:31\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-19 13:26:02\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-18 13:26:53\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-17 13:27:05\nFrom: 2026-04-16 00:00:00\nTo: 2026-04-16 23:59:59\nroot@docker_lamp_1:/home/jiminny# \nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ php artisan jiminny:debug\nzsh: command not found: php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nCount: 1\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nCount: 1\nAll count: 1\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 4.21ms DONE\n cache ............................................................................................................................... 24.90ms DONE\n compiled ............................................................................................................................. 2.45ms DONE\n events ............................................................................................................................... 1.93ms DONE\n routes ............................................................................................................................... 2.01ms DONE\n views ................................................................................................................................ 9.92ms DONE\n\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nCount: 6\nAll count: 6\nroot@docker_lamp_1:/home/jiminny#","depth":4,"value":"root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-21 13:25:21\nFrom: 2026-04-20 00:00:00\nTo: 2026-04-20 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-20 13:25:31\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-19 13:26:02\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-18 13:26:53\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-17 13:27:05\nFrom: 2026-04-16 00:00:00\nTo: 2026-04-16 23:59:59\nroot@docker_lamp_1:/home/jiminny# \nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ php artisan jiminny:debug\nzsh: command not found: php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nCount: 1\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nCount: 1\nAll count: 1\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 4.21ms DONE\n cache ............................................................................................................................... 24.90ms DONE\n compiled ............................................................................................................................. 2.45ms DONE\n events ............................................................................................................................... 1.93ms DONE\n routes ............................................................................................................................... 2.01ms DONE\n views ................................................................................................................................ 9.92ms DONE\n\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nCount: 6\nAll count: 6\nroot@docker_lamp_1:/home/jiminny#","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"docker","depth":2,"bounds":{"left":0.12291667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.12708333,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.24583334,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.25,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"✳ Build full day activity summary from Screenpipe (claude)","depth":2,"bounds":{"left":0.36875,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.37291667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.49166667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.49583334,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.6145833,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.61875,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.7375,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.7416667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ec2-user@ip-10-30-159-186:~ (-zsh)","depth":2,"bounds":{"left":0.86041665,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8645833,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"docker","depth":1,"bounds":{"left":0.4826389,"top":0.033333335,"width":0.034027778,"height":0.017777778},"role_description":"text"}]...
|
-2247217100414710928
|
409341081650104654
|
idle
|
accessibility
|
NULL
|
root@docker_lamp_1:/home/jiminny# php artisan jimi root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-21 13:25:21
From: 2026-04-20 00:00:00
To: 2026-04-20 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-20 13:25:31
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-19 13:26:02
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-18 13:26:53
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-17 13:27:05
From: 2026-04-16 00:00:00
To: 2026-04-16 23:59:59
root@docker_lamp_1:/home/jiminny#
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66
Learn more at [URL_WITH_CREDENTIALS] ~ $ dev
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Count: 1
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Count: 1
All count: 1
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 4.21ms DONE
cache [PASSWORD_DOTS] 24.90ms DONE
compiled [PASSWORD_DOTS] 2.45ms DONE
events [PASSWORD_DOTS] 1.93ms DONE
routes [PASSWORD_DOTS] 2.01ms DONE
views [PASSWORD_DOTS] 9.92ms DONE
worker-download:worker-download_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-nudges:worker-nudges_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Count: 6
All count: 6
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
docker
Close Tab
-zsh
Close Tab
✳ Build full day activity summary from Screenpipe (claude)
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-159-186:~ (-zsh)
Close Tab
⌥⌘1
docker...
|
73679
|
|
73680
|
NULL
|
0
|
2026-04-23T08:17:19.285592+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776932239285_m2.jpg...
|
iTerm2
|
docker
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
root@docker_lamp_1:/home/jiminny# php artisan jimi root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-21 13:25:21
From: 2026-04-20 00:00:00
To: 2026-04-20 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-20 13:25:31
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-19 13:26:02
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-18 13:26:53
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-17 13:27:05
From: 2026-04-16 00:00:00
To: 2026-04-16 23:59:59
root@docker_lamp_1:/home/jiminny#
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66
Learn more at [URL_WITH_CREDENTIALS] ~ $ dev
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Count: 1
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Count: 1
All count: 1
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 4.21ms DONE
cache [PASSWORD_DOTS] 24.90ms DONE
compiled [PASSWORD_DOTS] 2.45ms DONE
events [PASSWORD_DOTS] 1.93ms DONE
routes [PASSWORD_DOTS] 2.01ms DONE
views [PASSWORD_DOTS] 9.92ms DONE
worker-download:worker-download_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-nudges:worker-nudges_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Count: 6
All count: 6
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
docker
Close Tab
-zsh
Close Tab
✳ Build full day activity summary from Screenpipe (claude)
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-159-186:~ (-zsh)
Close Tab
⌥⌘1
docker...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-21 13:25:21\nFrom: 2026-04-20 00:00:00\nTo: 2026-04-20 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-20 13:25:31\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-19 13:26:02\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-18 13:26:53\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-17 13:27:05\nFrom: 2026-04-16 00:00:00\nTo: 2026-04-16 23:59:59\nroot@docker_lamp_1:/home/jiminny# \nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ php artisan jiminny:debug\nzsh: command not found: php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nCount: 1\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nCount: 1\nAll count: 1\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 4.21ms DONE\n cache ............................................................................................................................... 24.90ms DONE\n compiled ............................................................................................................................. 2.45ms DONE\n events ............................................................................................................................... 1.93ms DONE\n routes ............................................................................................................................... 2.01ms DONE\n views ................................................................................................................................ 9.92ms DONE\n\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nCount: 6\nAll count: 6\nroot@docker_lamp_1:/home/jiminny#","depth":4,"bounds":{"left":0.27027926,"top":0.5171588,"width":0.4787234,"height":0.4828412},"value":"root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-21 13:25:21\nFrom: 2026-04-20 00:00:00\nTo: 2026-04-20 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-20 13:25:31\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-19 13:26:02\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-18 13:26:53\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-17 13:27:05\nFrom: 2026-04-16 00:00:00\nTo: 2026-04-16 23:59:59\nroot@docker_lamp_1:/home/jiminny# \nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ php artisan jiminny:debug\nzsh: command not found: php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nCount: 1\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nCount: 1\nAll count: 1\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 4.21ms DONE\n cache ............................................................................................................................... 24.90ms DONE\n compiled ............................................................................................................................. 2.45ms DONE\n events ............................................................................................................................... 1.93ms DONE\n routes ............................................................................................................................... 2.01ms DONE\n views ................................................................................................................................ 9.92ms DONE\n\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nCount: 6\nAll count: 6\nroot@docker_lamp_1:/home/jiminny#","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"docker","depth":2,"bounds":{"left":0.32912233,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.33111703,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.3879654,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.3899601,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"✳ Build full day activity summary from Screenpipe (claude)","depth":2,"bounds":{"left":0.44680852,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4488032,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.5056516,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.50764626,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.56449467,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.56648934,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.62333775,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6253325,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ec2-user@ip-10-30-159-186:~ (-zsh)","depth":2,"bounds":{"left":0.6821808,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.68417555,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7273936,"top":1.0,"width":0.01861702,"height":-0.023144484},"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"docker","depth":1,"bounds":{"left":0.5013298,"top":1.0,"width":0.016289894,"height":-0.02394259},"role_description":"text"}]...
|
-2247217100414710928
|
409341081650104654
|
idle
|
accessibility
|
NULL
|
root@docker_lamp_1:/home/jiminny# php artisan jimi root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-21 13:25:21
From: 2026-04-20 00:00:00
To: 2026-04-20 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-20 13:25:31
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-19 13:26:02
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-18 13:26:53
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-17 13:27:05
From: 2026-04-16 00:00:00
To: 2026-04-16 23:59:59
root@docker_lamp_1:/home/jiminny#
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66
Learn more at [URL_WITH_CREDENTIALS] ~ $ dev
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Count: 1
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Count: 1
All count: 1
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 4.21ms DONE
cache [PASSWORD_DOTS] 24.90ms DONE
compiled [PASSWORD_DOTS] 2.45ms DONE
events [PASSWORD_DOTS] 1.93ms DONE
routes [PASSWORD_DOTS] 2.01ms DONE
views [PASSWORD_DOTS] 9.92ms DONE
worker-download:worker-download_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-nudges:worker-nudges_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Count: 6
All count: 6
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
docker
Close Tab
-zsh
Close Tab
✳ Build full day activity summary from Screenpipe (claude)
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-159-186:~ (-zsh)
Close Tab
⌥⌘1
docker...
|
NULL
|
|
73651
|
NULL
|
0
|
2026-04-23T08:12:13.083589+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776931933083_m1.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsRepository.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
16
7
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active Ask Jiminny reports whose expiry date has passed.
*
* @return Collection<AutomatedReport>
*/
public function getExpiredActiveAskJiminnyReports(): Collection
{
return AutomatedReport::where('status', true)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereNotNull('expires_at')
->where('expires_at', '<', now()->toDateString())
->get();
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('report_id', $report->getId())
->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])
->latest()
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
public function countAllUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->count();
}
/**
* Restrict a query on the automated_reports table to reports the given user is allowed to see.
*
* Matches the customer-facing audience:
* - explicit user recipients (recipients.users)
* - members of any of the report's groups (Ask Jiminny reports)
*/
private function applyUserAccessScope(Builder $query, User $user): void
{
$userId = $user->getId();
$groupId = $user->getGroupId();
$query
->where('automated_reports.team_id', $user->getTeamId())
->where(function (Builder $q) use ($userId, $groupId): void {
$q->whereJsonContains('automated_reports.recipients->users', $userId);
if ($groupId !== null) {
$q->orWhere(function (Builder $sub) use ($groupId): void {
$sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereJsonContains('automated_reports.groups', $groupId);
});
}
});
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
19
15
2
4
Previous Highlighted Error
Next Highlighted Error
SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o
JOIN activities a ON o.id = a.opportunity_id
WHERE a.crm_configuration_id = 39
AND a.actual_start_time > '2025-10-13'
AND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM activities
WHERE crm_configuration_id = 39 and user_id = 143
and actual_start_time >= '2025-10-13'
AND type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM opportunities WHERE account_id IN (178);
select * from activities where id IN (620137, 620187, 620188, 620189, 620230);
# HS
SELECT * FROM opportunities WHERE id IN (238);
select * from activities where id IN (477,2076);
select * from users;
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM activities;
SELECT COUNT(*) FROM opportunities;
UPDATE activities
SET
actual_start_time = '2025-12-19 09:00:00',
actual_end_time = '2025-12-19 10:30:00',
scheduled_start_time = '2025-12-19 09:00:00',
scheduled_end_time = '2025-12-19 10:30:00'
WHERE id IN (407509,407375);
select * from partners;
SELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id
FROM activities
WHERE user_id = 143
AND actual_start_time >= '2025-10-13 00:00:00'
AND actual_start_time <= '2026-01-13 23:59:59'
ORDER BY actual_start_time DESC;
SELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;
SELECT * FROM crm_layouts where crm_configuration_id = 39;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;
# lead_id
# account_id 177
# contact_id 3969
# opportunity_id
# stage_id 203
SELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;
SELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'
AND user_id = 143 and actual_start_time >= '2025-10-13';
SELECT * FROM activities a
# JOIN opportunities o ON a.opportunity_id = o.id
WHERE a.crm_configuration_id = 39 AND a.type = 'conference'
and status = 'completed' and recording_state = 'recorded'
and a.actual_start_time >= '2025-10-13'
AND a.user_id = 143
;
select * from leads
where crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310);
SELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198
SELECT * FROM activities WHERE id IN (356001, 356008); # contacts:
SELECT * FROM opportunities WHERE id IN (1707);
SELECT * FROM stages where id IN (204, 198);
SELECT * FROM opportunities WHERE account_id IN (178);
SELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';
SELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal
SELECT * FROM activities where crm_configuration_id = 39
AND opportunity_id IS NULL
AND is_internal = false
and status = 'completed' and recording_state = 'recorded'
AND actual_start_time >= '2025-10-13'
AND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)
# AND lead_id IN (112, 109)
;
SELECT * FROM crm_profiles WHERE user_id = 143;
select * from inboxes; # 212
select * from users where id = 143; # 143
select * from inbox_email_batches where inbox_id = 212
and updated_at >= '2026-01-28 00:00:00' order by id desc;
select * from inbox_emails where inbox_id = 212
and batch_id = 95885 order by id desc;
select * from email_messages where origin_user_id = 143;
select * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';
select * from participants where activity_id = 620247;
select * from crm_profiles where user_id = 143;
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001
select * from transcription where activity_id = 356001; # 6943
select * from ai_prompts where transcription_id = 6943;
SELECT * FROM activity_summary_logs where activity_id = 356001;
SELECT * FROM social_accounts WHERE sociable_id = 143;
# [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;
# 422515 softphone tr. 8100
SELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;
# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS
select * from ai_prompts where transcription_id IN (8100, 7670);
select * from activity_summary_logs where activity_id = 407509;
select * from sidekick_settings;
select * from default_activity_types;
SELECT * FROM contacts WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM leads WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM activity_searches where user_id = 143;
SELECT * FROM groups where team_id = 1;
select * from teams where id = 1;
select * from groups where team_id = 1; # 1150 - 7e75f8025c22
select id, name, group_id, status, deleted_at, email
from users where team_id = 1 order by group_id desc ;
select * from activity_searches where id in (1977, 1978, 1979);
select * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);
select * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277
select * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879
INSERT INTO `activity_search_filters`
(`activity_search_id`, `filter`, `value`) VALUES
(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')
;
select * from crm_configurations where id = 39;
select sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id
where u.team_id = 1;
SELECT * FROM social_accounts WHERE sociable_id = 1635;
SELECT * FROM users WHERE id = 1635;
select * from teams where id = 1;
select * from users where team_id = 1;
select * from team_features where team_id = 1;
select * from features;
SELECT * FROM activity_searches where id = 1982; # 1981
SELECT * FROM activity_search_filters WHERE activity_search_id = 1982;
SELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;
SELECT * FROM automated_reports where id = 71;
SELECT * FROM automated_report_results where report_id = 71;
UPDATE automated_reports set playbook_categories = NULL where id = 68;
SELECT * FROM automated_report_results where id = 275;
SELECT * FROM automated_reports order by id desc;
SELECT * FROM automated_report_results order by id desc;
select * from activity_searches where user_id = 143;
select * from ask_anything_prompts;
SELECT * FROM groups WHERE id = 1439;
SELECT * FROM users WHERE group_id = 1439;
select * from permissions; # 158
select * from roles;
select * from permission_role
select * from teams where id = 1;
select * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;
select * from groups where id = 28;
select * from playbooks where team_id = 1;
select * from playbooks where id = 179;
select * from playbook_categories where id = 1391;
select * from users where id = 143;
select * from crm_profiles where user_id = 143;
select * from activities where crm_configuration_id = 39 and type = 'conference'
and crm_provider_id IS NOT NULL ORDER by id desc;
select * from activities where id = 422003; # 00UO400000pB6fpMAC
SELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type
FROM automated_report_results ar
JOIN automated_reports a ON a.id = ar.report_id
WHERE a.type = 'ask_jiminny'
LIMIT 10;
SELECT `automated_report_results`.* FROM `automated_report_results`
INNER JOIN `automated_reports`
ON `automated_report_results`.`report_id` = `automated_reports`.`id`
WHERE `automated_report_results`.`generated_at` IS NOT NULL
AND `automated_reports`.`team_id` = 1
AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$."users"')
;
select * from teams where id = 3143;
select * from crm_configurations where id = 500;
select * from users where name = 'Integration Account'; # 1695
SELECT * FROM social_accounts WHERE sociable_id = 1695;
select * from activities where crm_configuration_id = 39
and recording_state = 'recorded' and duration > 60
and status = 'completed' and actual_start_time >= '2025-12-01';
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;
select * from leads;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20157-AJ-report-not-send-notification, menu","depth":5,"help_text":"Git Branch: JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"RequestGenerateAskJiminnyReportJobTest","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"16","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"7","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active Ask Jiminny reports whose expiry date has passed.\n *\n * @return Collection<AutomatedReport>\n */\n public function getExpiredActiveAskJiminnyReports(): Collection\n {\n return AutomatedReport::where('status', true)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereNotNull('expires_at')\n ->where('expires_at', '<', now()->toDateString())\n ->get();\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('report_id', $report->getId())\n ->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])\n ->latest()\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n public function countAllUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->count();\n }\n\n /**\n * Restrict a query on the automated_reports table to reports the given user is allowed to see.\n *\n * Matches the customer-facing audience:\n * - explicit user recipients (recipients.users)\n * - members of any of the report's groups (Ask Jiminny reports)\n */\n private function applyUserAccessScope(Builder $query, User $user): void\n {\n $userId = $user->getId();\n $groupId = $user->getGroupId();\n\n $query\n ->where('automated_reports.team_id', $user->getTeamId())\n ->where(function (Builder $q) use ($userId, $groupId): void {\n $q->whereJsonContains('automated_reports.recipients->users', $userId);\n\n if ($groupId !== null) {\n $q->orWhere(function (Builder $sub) use ($groupId): void {\n $sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereJsonContains('automated_reports.groups', $groupId);\n });\n }\n });\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active Ask Jiminny reports whose expiry date has passed.\n *\n * @return Collection<AutomatedReport>\n */\n public function getExpiredActiveAskJiminnyReports(): Collection\n {\n return AutomatedReport::where('status', true)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereNotNull('expires_at')\n ->where('expires_at', '<', now()->toDateString())\n ->get();\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('report_id', $report->getId())\n ->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])\n ->latest()\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n public function countAllUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->count();\n }\n\n /**\n * Restrict a query on the automated_reports table to reports the given user is allowed to see.\n *\n * Matches the customer-facing audience:\n * - explicit user recipients (recipients.users)\n * - members of any of the report's groups (Ask Jiminny reports)\n */\n private function applyUserAccessScope(Builder $query, User $user): void\n {\n $userId = $user->getId();\n $groupId = $user->getGroupId();\n\n $query\n ->where('automated_reports.team_id', $user->getTeamId())\n ->where(function (Builder $q) use ($userId, $groupId): void {\n $q->whereJsonContains('automated_reports.recipients->users', $userId);\n\n if ($groupId !== null) {\n $q->orWhere(function (Builder $sub) use ($groupId): void {\n $sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereJsonContains('automated_reports.groups', $groupId);\n });\n }\n });\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"15","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE `automated_report_results`.`generated_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;","depth":4,"value":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE `automated_report_results`.`generated_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-962688942467523245
|
-7004754253361826235
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
16
7
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active Ask Jiminny reports whose expiry date has passed.
*
* @return Collection<AutomatedReport>
*/
public function getExpiredActiveAskJiminnyReports(): Collection
{
return AutomatedReport::where('status', true)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereNotNull('expires_at')
->where('expires_at', '<', now()->toDateString())
->get();
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('report_id', $report->getId())
->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])
->latest()
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
public function countAllUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->count();
}
/**
* Restrict a query on the automated_reports table to reports the given user is allowed to see.
*
* Matches the customer-facing audience:
* - explicit user recipients (recipients.users)
* - members of any of the report's groups (Ask Jiminny reports)
*/
private function applyUserAccessScope(Builder $query, User $user): void
{
$userId = $user->getId();
$groupId = $user->getGroupId();
$query
->where('automated_reports.team_id', $user->getTeamId())
->where(function (Builder $q) use ($userId, $groupId): void {
$q->whereJsonContains('automated_reports.recipients->users', $userId);
if ($groupId !== null) {
$q->orWhere(function (Builder $sub) use ($groupId): void {
$sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereJsonContains('automated_reports.groups', $groupId);
});
}
});
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
19
15
2
4
Previous Highlighted Error
Next Highlighted Error
SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o
JOIN activities a ON o.id = a.opportunity_id
WHERE a.crm_configuration_id = 39
AND a.actual_start_time > '2025-10-13'
AND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM activities
WHERE crm_configuration_id = 39 and user_id = 143
and actual_start_time >= '2025-10-13'
AND type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM opportunities WHERE account_id IN (178);
select * from activities where id IN (620137, 620187, 620188, 620189, 620230);
# HS
SELECT * FROM opportunities WHERE id IN (238);
select * from activities where id IN (477,2076);
select * from users;
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM activities;
SELECT COUNT(*) FROM opportunities;
UPDATE activities
SET
actual_start_time = '2025-12-19 09:00:00',
actual_end_time = '2025-12-19 10:30:00',
scheduled_start_time = '2025-12-19 09:00:00',
scheduled_end_time = '2025-12-19 10:30:00'
WHERE id IN (407509,407375);
select * from partners;
SELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id
FROM activities
WHERE user_id = 143
AND actual_start_time >= '2025-10-13 00:00:00'
AND actual_start_time <= '2026-01-13 23:59:59'
ORDER BY actual_start_time DESC;
SELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;
SELECT * FROM crm_layouts where crm_configuration_id = 39;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;
# lead_id
# account_id 177
# contact_id 3969
# opportunity_id
# stage_id 203
SELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;
SELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'
AND user_id = 143 and actual_start_time >= '2025-10-13';
SELECT * FROM activities a
# JOIN opportunities o ON a.opportunity_id = o.id
WHERE a.crm_configuration_id = 39 AND a.type = 'conference'
and status = 'completed' and recording_state = 'recorded'
and a.actual_start_time >= '2025-10-13'
AND a.user_id = 143
;
select * from leads
where crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310);
SELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198
SELECT * FROM activities WHERE id IN (356001, 356008); # contacts:
SELECT * FROM opportunities WHERE id IN (1707);
SELECT * FROM stages where id IN (204, 198);
SELECT * FROM opportunities WHERE account_id IN (178);
SELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';
SELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal
SELECT * FROM activities where crm_configuration_id = 39
AND opportunity_id IS NULL
AND is_internal = false
and status = 'completed' and recording_state = 'recorded'
AND actual_start_time >= '2025-10-13'
AND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)
# AND lead_id IN (112, 109)
;
SELECT * FROM crm_profiles WHERE user_id = 143;
select * from inboxes; # 212
select * from users where id = 143; # 143
select * from inbox_email_batches where inbox_id = 212
and updated_at >= '2026-01-28 00:00:00' order by id desc;
select * from inbox_emails where inbox_id = 212
and batch_id = 95885 order by id desc;
select * from email_messages where origin_user_id = 143;
select * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';
select * from participants where activity_id = 620247;
select * from crm_profiles where user_id = 143;
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001
select * from transcription where activity_id = 356001; # 6943
select * from ai_prompts where transcription_id = 6943;
SELECT * FROM activity_summary_logs where activity_id = 356001;
SELECT * FROM social_accounts WHERE sociable_id = 143;
# [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;
# 422515 softphone tr. 8100
SELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;
# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS
select * from ai_prompts where transcription_id IN (8100, 7670);
select * from activity_summary_logs where activity_id = 407509;
select * from sidekick_settings;
select * from default_activity_types;
SELECT * FROM contacts WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM leads WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM activity_searches where user_id = 143;
SELECT * FROM groups where team_id = 1;
select * from teams where id = 1;
select * from groups where team_id = 1; # 1150 - 7e75f8025c22
select id, name, group_id, status, deleted_at, email
from users where team_id = 1 order by group_id desc ;
select * from activity_searches where id in (1977, 1978, 1979);
select * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);
select * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277
select * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879
INSERT INTO `activity_search_filters`
(`activity_search_id`, `filter`, `value`) VALUES
(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')
;
select * from crm_configurations where id = 39;
select sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id
where u.team_id = 1;
SELECT * FROM social_accounts WHERE sociable_id = 1635;
SELECT * FROM users WHERE id = 1635;
select * from teams where id = 1;
select * from users where team_id = 1;
select * from team_features where team_id = 1;
select * from features;
SELECT * FROM activity_searches where id = 1982; # 1981
SELECT * FROM activity_search_filters WHERE activity_search_id = 1982;
SELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;
SELECT * FROM automated_reports where id = 71;
SELECT * FROM automated_report_results where report_id = 71;
UPDATE automated_reports set playbook_categories = NULL where id = 68;
SELECT * FROM automated_report_results where id = 275;
SELECT * FROM automated_reports order by id desc;
SELECT * FROM automated_report_results order by id desc;
select * from activity_searches where user_id = 143;
select * from ask_anything_prompts;
SELECT * FROM groups WHERE id = 1439;
SELECT * FROM users WHERE group_id = 1439;
select * from permissions; # 158
select * from roles;
select * from permission_role
select * from teams where id = 1;
select * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;
select * from groups where id = 28;
select * from playbooks where team_id = 1;
select * from playbooks where id = 179;
select * from playbook_categories where id = 1391;
select * from users where id = 143;
select * from crm_profiles where user_id = 143;
select * from activities where crm_configuration_id = 39 and type = 'conference'
and crm_provider_id IS NOT NULL ORDER by id desc;
select * from activities where id = 422003; # 00UO400000pB6fpMAC
SELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type
FROM automated_report_results ar
JOIN automated_reports a ON a.id = ar.report_id
WHERE a.type = 'ask_jiminny'
LIMIT 10;
SELECT `automated_report_results`.* FROM `automated_report_results`
INNER JOIN `automated_reports`
ON `automated_report_results`.`report_id` = `automated_reports`.`id`
WHERE `automated_report_results`.`generated_at` IS NOT NULL
AND `automated_reports`.`team_id` = 1
AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$."users"')
;
select * from teams where id = 3143;
select * from crm_configurations where id = 500;
select * from users where name = 'Integration Account'; # 1695
SELECT * FROM social_accounts WHERE sociable_id = 1695;
select * from activities where crm_configuration_id = 39
and recording_state = 'recorded' and duration > 60
and status = 'completed' and actual_start_time >= '2025-12-01';
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;
select * from leads;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
73649
|
|
73650
|
NULL
|
0
|
2026-04-23T08:12:09.098606+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776931929098_m2.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsRepository.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
16
7
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active Ask Jiminny reports whose expiry date has passed.
*
* @return Collection<AutomatedReport>
*/
public function getExpiredActiveAskJiminnyReports(): Collection
{
return AutomatedReport::where('status', true)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereNotNull('expires_at')
->where('expires_at', '<', now()->toDateString())
->get();
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('report_id', $report->getId())
->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])
->latest()
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
public function countAllUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->count();
}
/**
* Restrict a query on the automated_reports table to reports the given user is allowed to see.
*
* Matches the customer-facing audience:
* - explicit user recipients (recipients.users)
* - members of any of the report's groups (Ask Jiminny reports)
*/
private function applyUserAccessScope(Builder $query, User $user): void
{
$userId = $user->getId();
$groupId = $user->getGroupId();
$query
->where('automated_reports.team_id', $user->getTeamId())
->where(function (Builder $q) use ($userId, $groupId): void {
$q->whereJsonContains('automated_reports.recipients->users', $userId);
if ($groupId !== null) {
$q->orWhere(function (Builder $sub) use ($groupId): void {
$sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereJsonContains('automated_reports.groups', $groupId);
});
}
});
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.25731382,"top":0.019952115,"width":0.03856383,"height":0.025538707},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20157-AJ-report-not-send-notification, menu","depth":5,"bounds":{"left":0.29587767,"top":0.019952115,"width":0.10139628,"height":0.025538707},"help_text":"Git Branch: JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.7972075,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"RequestGenerateAskJiminnyReportJobTest","depth":6,"bounds":{"left":0.8125,"top":0.019952115,"width":0.10305851,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"16","depth":4,"bounds":{"left":0.60638297,"top":0.17478053,"width":0.009640957,"height":0.015163607},"role_description":"text"},{"role":"AXStaticText","text":"7","depth":4,"bounds":{"left":0.6180186,"top":0.17478053,"width":0.0076462766,"height":0.015163607},"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.62765956,"top":0.17478053,"width":0.00731383,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.63663566,"top":0.17318435,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.64394945,"top":0.17318435,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active Ask Jiminny reports whose expiry date has passed.\n *\n * @return Collection<AutomatedReport>\n */\n public function getExpiredActiveAskJiminnyReports(): Collection\n {\n return AutomatedReport::where('status', true)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereNotNull('expires_at')\n ->where('expires_at', '<', now()->toDateString())\n ->get();\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('report_id', $report->getId())\n ->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])\n ->latest()\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n public function countAllUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->count();\n }\n\n /**\n * Restrict a query on the automated_reports table to reports the given user is allowed to see.\n *\n * Matches the customer-facing audience:\n * - explicit user recipients (recipients.users)\n * - members of any of the report's groups (Ask Jiminny reports)\n */\n private function applyUserAccessScope(Builder $query, User $user): void\n {\n $userId = $user->getId();\n $groupId = $user->getGroupId();\n\n $query\n ->where('automated_reports.team_id', $user->getTeamId())\n ->where(function (Builder $q) use ($userId, $groupId): void {\n $q->whereJsonContains('automated_reports.recipients->users', $userId);\n\n if ($groupId !== null) {\n $q->orWhere(function (Builder $sub) use ($groupId): void {\n $sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereJsonContains('automated_reports.groups', $groupId);\n });\n }\n });\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active Ask Jiminny reports whose expiry date has passed.\n *\n * @return Collection<AutomatedReport>\n */\n public function getExpiredActiveAskJiminnyReports(): Collection\n {\n return AutomatedReport::where('status', true)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereNotNull('expires_at')\n ->where('expires_at', '<', now()->toDateString())\n ->get();\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('report_id', $report->getId())\n ->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])\n ->latest()\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n public function countAllUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->count();\n }\n\n /**\n * Restrict a query on the automated_reports table to reports the given user is allowed to see.\n *\n * Matches the customer-facing audience:\n * - explicit user recipients (recipients.users)\n * - members of any of the report's groups (Ask Jiminny reports)\n */\n private function applyUserAccessScope(Builder $query, User $user): void\n {\n $userId = $user->getId();\n $groupId = $user->getGroupId();\n\n $query\n ->where('automated_reports.team_id', $user->getTeamId())\n ->where(function (Builder $q) use ($userId, $groupId): void {\n $q->whereJsonContains('automated_reports.recipients->users', $userId);\n\n if ($groupId !== null) {\n $q->orWhere(function (Builder $sub) use ($groupId): void {\n $sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereJsonContains('automated_reports.groups', $groupId);\n });\n }\n });\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.6525931,"top":0.09896249,"width":0.008643617,"height":0.01915403},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.6612367,"top":0.09896249,"width":0.008643617,"height":0.01915403},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.6722075,"top":0.09896249,"width":0.008643617,"height":0.01915403},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.68085104,"top":0.09896249,"width":0.008643617,"height":0.01915403},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.68949467,"top":0.09896249,"width":0.008643617,"height":0.01915403},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.70046544,"top":0.09896249,"width":0.008643617,"height":0.01915403},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.71143615,"top":0.09896249,"width":0.024268618,"height":0.01915403},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.7380319,"top":0.09896249,"width":0.008643617,"height":0.01915403},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.74900264,"top":0.09896249,"width":0.029587766,"height":0.01915403},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.9587766,"top":0.09896249,"width":0.02825798,"height":0.01915403},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-1474289408620756313
|
-8921860298346910096
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
16
7
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active Ask Jiminny reports whose expiry date has passed.
*
* @return Collection<AutomatedReport>
*/
public function getExpiredActiveAskJiminnyReports(): Collection
{
return AutomatedReport::where('status', true)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereNotNull('expires_at')
->where('expires_at', '<', now()->toDateString())
->get();
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('report_id', $report->getId())
->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])
->latest()
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
public function countAllUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->count();
}
/**
* Restrict a query on the automated_reports table to reports the given user is allowed to see.
*
* Matches the customer-facing audience:
* - explicit user recipients (recipients.users)
* - members of any of the report's groups (Ask Jiminny reports)
*/
private function applyUserAccessScope(Builder $query, User $user): void
{
$userId = $user->getId();
$groupId = $user->getGroupId();
$query
->where('automated_reports.team_id', $user->getTeamId())
->where(function (Builder $q) use ($userId, $groupId): void {
$q->whereJsonContains('automated_reports.recipients->users', $userId);
if ($groupId !== null) {
$q->orWhere(function (Builder $sub) use ($groupId): void {
$sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereJsonContains('automated_reports.groups', $groupId);
});
}
});
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny...
|
NULL
|
|
73626
|
NULL
|
0
|
2026-04-23T08:07:04.234089+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776931624234_m2.jpg...
|
iTerm2
|
docker
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
root@docker_lamp_1:/home/jiminny# php artisan jimi root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-21 13:25:21
From: 2026-04-20 00:00:00
To: 2026-04-20 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-20 13:25:31
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-19 13:26:02
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-18 13:26:53
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-17 13:27:05
From: 2026-04-16 00:00:00
To: 2026-04-16 23:59:59
root@docker_lamp_1:/home/jiminny#
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66
Learn more at [URL_WITH_CREDENTIALS] ~ $ dev
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Count: 1
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Count: 1
All count: 1
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
docker
Close Tab
-zsh
Close Tab
✳ Build full day activity summary from Screenpipe (claude)
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-159-186:~ (-zsh)
Close Tab
⌥⌘1
docker...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-21 13:25:21\nFrom: 2026-04-20 00:00:00\nTo: 2026-04-20 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-20 13:25:31\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-19 13:26:02\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-18 13:26:53\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-17 13:27:05\nFrom: 2026-04-16 00:00:00\nTo: 2026-04-16 23:59:59\nroot@docker_lamp_1:/home/jiminny# \nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ php artisan jiminny:debug\nzsh: command not found: php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nCount: 1\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nCount: 1\nAll count: 1\nroot@docker_lamp_1:/home/jiminny#","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.4787234,"height":-0.06304872},"value":"root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-21 13:25:21\nFrom: 2026-04-20 00:00:00\nTo: 2026-04-20 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-20 13:25:31\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-19 13:26:02\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-18 13:26:53\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-17 13:27:05\nFrom: 2026-04-16 00:00:00\nTo: 2026-04-16 23:59:59\nroot@docker_lamp_1:/home/jiminny# \nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ php artisan jiminny:debug\nzsh: command not found: php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nCount: 1\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nCount: 1\nAll count: 1\nroot@docker_lamp_1:/home/jiminny#","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"docker","depth":2,"bounds":{"left":0.32912233,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.33111703,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.3879654,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.3899601,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"✳ Build full day activity summary from Screenpipe (claude)","depth":2,"bounds":{"left":0.44680852,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4488032,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.5056516,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.50764626,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.56449467,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.56648934,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.62333775,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6253325,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ec2-user@ip-10-30-159-186:~ (-zsh)","depth":2,"bounds":{"left":0.6821808,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.68417555,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7273936,"top":1.0,"width":0.01861702,"height":-0.023144484},"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"docker","depth":1,"bounds":{"left":0.5013298,"top":1.0,"width":0.016289894,"height":-0.02394259},"role_description":"text"}]...
|
7668107452558903547
|
110141427734317899
|
idle
|
accessibility
|
NULL
|
root@docker_lamp_1:/home/jiminny# php artisan jimi root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-21 13:25:21
From: 2026-04-20 00:00:00
To: 2026-04-20 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-20 13:25:31
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-19 13:26:02
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-18 13:26:53
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-17 13:27:05
From: 2026-04-16 00:00:00
To: 2026-04-16 23:59:59
root@docker_lamp_1:/home/jiminny#
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66
Learn more at [URL_WITH_CREDENTIALS] ~ $ dev
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Count: 1
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Count: 1
All count: 1
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
docker
Close Tab
-zsh
Close Tab
✳ Build full day activity summary from Screenpipe (claude)
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-159-186:~ (-zsh)
Close Tab
⌥⌘1
docker...
|
NULL
|
|
73625
|
NULL
|
0
|
2026-04-23T08:07:01.776473+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776931621776_m1.jpg...
|
iTerm2
|
docker
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
root@docker_lamp_1:/home/jiminny# php artisan jimi root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-21 13:25:21
From: 2026-04-20 00:00:00
To: 2026-04-20 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-20 13:25:31
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-19 13:26:02
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-18 13:26:53
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-17 13:27:05
From: 2026-04-16 00:00:00
To: 2026-04-16 23:59:59
root@docker_lamp_1:/home/jiminny#
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66
Learn more at [URL_WITH_CREDENTIALS] ~ $ dev
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Count: 1
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Count: 1
All count: 1
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
docker
Close Tab
-zsh
Close Tab
✳ Build full day activity summary from Screenpipe (claude)
Close Tab
ffmpeg
Close Tab
-zsh
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-159-186:~ (-zsh)
Close Tab
⌥⌘1
docker...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-21 13:25:21\nFrom: 2026-04-20 00:00:00\nTo: 2026-04-20 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-20 13:25:31\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-19 13:26:02\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-18 13:26:53\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-17 13:27:05\nFrom: 2026-04-16 00:00:00\nTo: 2026-04-16 23:59:59\nroot@docker_lamp_1:/home/jiminny# \nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ php artisan jiminny:debug\nzsh: command not found: php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nCount: 1\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nCount: 1\nAll count: 1\nroot@docker_lamp_1:/home/jiminny#","depth":4,"bounds":{"left":0.0,"top":0.08777778,"width":1.0,"height":0.9122222},"value":"root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-21 13:25:21\nFrom: 2026-04-20 00:00:00\nTo: 2026-04-20 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-20 13:25:31\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-19 13:26:02\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-18 13:26:53\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-17 13:27:05\nFrom: 2026-04-16 00:00:00\nTo: 2026-04-16 23:59:59\nroot@docker_lamp_1:/home/jiminny# \nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ php artisan jiminny:debug\nzsh: command not found: php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nCount: 1\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nCount: 1\nAll count: 1\nroot@docker_lamp_1:/home/jiminny#","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"docker","depth":2,"bounds":{"left":0.12291667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.12708333,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.24583334,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.25,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"✳ Build full day activity summary from Screenpipe (claude)","depth":2,"bounds":{"left":0.36875,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.37291667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ffmpeg","depth":2,"bounds":{"left":0.49166667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.49583334,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.6145833,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.61875,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.7375,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.7416667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ec2-user@ip-10-30-159-186:~ (-zsh)","depth":2,"bounds":{"left":0.86041665,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8645833,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"docker","depth":1,"bounds":{"left":0.4826389,"top":0.033333335,"width":0.034027778,"height":0.017777778},"role_description":"text"}]...
|
7307813558940027369
|
110141427734448975
|
idle
|
accessibility
|
NULL
|
root@docker_lamp_1:/home/jiminny# php artisan jimi root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-21 13:25:21
From: 2026-04-20 00:00:00
To: 2026-04-20 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-20 13:25:31
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-19 13:26:02
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-18 13:26:53
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-17 13:27:05
From: 2026-04-16 00:00:00
To: 2026-04-16 23:59:59
root@docker_lamp_1:/home/jiminny#
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66
Learn more at [URL_WITH_CREDENTIALS] ~ $ dev
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Count: 1
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Count: 1
All count: 1
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
docker
Close Tab
-zsh
Close Tab
✳ Build full day activity summary from Screenpipe (claude)
Close Tab
ffmpeg
Close Tab
-zsh
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-159-186:~ (-zsh)
Close Tab
⌥⌘1
docker...
|
73623
|
|
73594
|
NULL
|
0
|
2026-04-23T08:01:58.360141+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776931318360_m1.jpg...
|
iTerm2
|
docker
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
root@docker_lamp_1:/home/jiminny# php artisan jimi root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-21 13:25:21
From: 2026-04-20 00:00:00
To: 2026-04-20 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-20 13:25:31
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-19 13:26:02
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-18 13:26:53
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-17 13:27:05
From: 2026-04-16 00:00:00
To: 2026-04-16 23:59:59
root@docker_lamp_1:/home/jiminny#
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66
Learn more at [URL_WITH_CREDENTIALS] ~ $ dev
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
docker
Close Tab
-zsh
Close Tab
✳ Build full day activity summary from Screenpipe (claude)
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-159-186:~ (-zsh)
Close Tab
⌥⌘1
docker...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-21 13:25:21\nFrom: 2026-04-20 00:00:00\nTo: 2026-04-20 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-20 13:25:31\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-19 13:26:02\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-18 13:26:53\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-17 13:27:05\nFrom: 2026-04-16 00:00:00\nTo: 2026-04-16 23:59:59\nroot@docker_lamp_1:/home/jiminny# \nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ php artisan jiminny:debug\nzsh: command not found: php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ dev\nroot@docker_lamp_1:/home/jiminny#","depth":4,"bounds":{"left":0.0,"top":0.08777778,"width":1.0,"height":0.9122222},"value":"root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-21 13:25:21\nFrom: 2026-04-20 00:00:00\nTo: 2026-04-20 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-20 13:25:31\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-19 13:26:02\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-18 13:26:53\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-17 13:27:05\nFrom: 2026-04-16 00:00:00\nTo: 2026-04-16 23:59:59\nroot@docker_lamp_1:/home/jiminny# \nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ php artisan jiminny:debug\nzsh: command not found: php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ dev\nroot@docker_lamp_1:/home/jiminny#","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"docker","depth":2,"bounds":{"left":0.12291667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.12708333,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.24583334,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.25,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"✳ Build full day activity summary from Screenpipe (claude)","depth":2,"bounds":{"left":0.36875,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.37291667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.49166667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.49583334,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.6145833,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.61875,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.7375,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.7416667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ec2-user@ip-10-30-159-186:~ (-zsh)","depth":2,"bounds":{"left":0.86041665,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8645833,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"docker","depth":1,"bounds":{"left":0.4826389,"top":0.033333335,"width":0.034027778,"height":0.017777778},"role_description":"text"}]...
|
-4235061640576757207
|
110141427735563087
|
idle
|
accessibility
|
NULL
|
root@docker_lamp_1:/home/jiminny# php artisan jimi root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-21 13:25:21
From: 2026-04-20 00:00:00
To: 2026-04-20 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-20 13:25:31
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-19 13:26:02
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-18 13:26:53
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-17 13:27:05
From: 2026-04-16 00:00:00
To: 2026-04-16 23:59:59
root@docker_lamp_1:/home/jiminny#
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66
Learn more at [URL_WITH_CREDENTIALS] ~ $ dev
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
docker
Close Tab
-zsh
Close Tab
✳ Build full day activity summary from Screenpipe (claude)
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-159-186:~ (-zsh)
Close Tab
⌥⌘1
docker...
|
73580
|
|
73593
|
NULL
|
0
|
2026-04-23T08:01:54.630698+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776931314630_m2.jpg...
|
iTerm2
|
docker
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
root@docker_lamp_1:/home/jiminny# php artisan jimi root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-21 13:25:21
From: 2026-04-20 00:00:00
To: 2026-04-20 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-20 13:25:31
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-19 13:26:02
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-18 13:26:53
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-17 13:27:05
From: 2026-04-16 00:00:00
To: 2026-04-16 23:59:59
root@docker_lamp_1:/home/jiminny#
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66
Learn more at [URL_WITH_CREDENTIALS] ~ $ dev
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
docker
Close Tab
-zsh
Close Tab
✳ Build full day activity summary from Screenpipe (claude)
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-159-186:~ (-zsh)
Close Tab
⌥⌘1
docker...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-21 13:25:21\nFrom: 2026-04-20 00:00:00\nTo: 2026-04-20 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-20 13:25:31\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-19 13:26:02\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-18 13:26:53\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-17 13:27:05\nFrom: 2026-04-16 00:00:00\nTo: 2026-04-16 23:59:59\nroot@docker_lamp_1:/home/jiminny# \nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ php artisan jiminny:debug\nzsh: command not found: php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ dev\nroot@docker_lamp_1:/home/jiminny#","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.4787234,"height":-0.06304872},"value":"root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-21 13:25:21\nFrom: 2026-04-20 00:00:00\nTo: 2026-04-20 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-20 13:25:31\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-19 13:26:02\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-18 13:26:53\nFrom: 2026-04-17 00:00:00\nTo: 2026-04-17 23:59:59\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nNow: 2026-04-17 13:27:05\nFrom: 2026-04-16 00:00:00\nTo: 2026-04-16 23:59:59\nroot@docker_lamp_1:/home/jiminny# \nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ php artisan jiminny:debug\nzsh: command not found: php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ dev\nroot@docker_lamp_1:/home/jiminny#","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"docker","depth":2,"bounds":{"left":0.32912233,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.33111703,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.3879654,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.3899601,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"✳ Build full day activity summary from Screenpipe (claude)","depth":2,"bounds":{"left":0.44680852,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4488032,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.5056516,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.50764626,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.56449467,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.56648934,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.62333775,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6253325,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ec2-user@ip-10-30-159-186:~ (-zsh)","depth":2,"bounds":{"left":0.6821808,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.68417555,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7273936,"top":1.0,"width":0.01861702,"height":-0.023144484},"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"docker","depth":1,"bounds":{"left":0.5013298,"top":1.0,"width":0.016289894,"height":-0.02394259},"role_description":"text"}]...
|
-4235061640576757207
|
110141427735563087
|
idle
|
accessibility
|
NULL
|
root@docker_lamp_1:/home/jiminny# php artisan jimi root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-21 13:25:21
From: 2026-04-20 00:00:00
To: 2026-04-20 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-20 13:25:31
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-19 13:26:02
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-18 13:26:53
From: 2026-04-17 00:00:00
To: 2026-04-17 23:59:59
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Now: 2026-04-17 13:27:05
From: 2026-04-16 00:00:00
To: 2026-04-16 23:59:59
root@docker_lamp_1:/home/jiminny#
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66
Learn more at [URL_WITH_CREDENTIALS] ~ $ dev
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
docker
Close Tab
-zsh
Close Tab
✳ Build full day activity summary from Screenpipe (claude)
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-159-186:~ (-zsh)
Close Tab
⌥⌘1
docker...
|
73579
|
|
73568
|
NULL
|
0
|
2026-04-23T07:56:52.299597+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776931012299_m2.jpg...
|
iTerm2
|
DOCKER (docker-compose)
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
0 social account(s) to be processed ...
docker_lam 0 social account(s) to be processed ...
docker_lamp_1 |
docker_lamp_1 | Done!
docker_lamp_1 | 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:bullhorn:ping --heartbeat > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 |
docker_lamp_1 | 2026-04-22 15:41:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:41:04 Running ['artisan' dialers:monitor-activities] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:41:06 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:41:07 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:41:08 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:41:10 Running ['artisan' crm:sync-hubspot-objects] [HubSpot] Syncing objects for Hubspot (abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4) since 2026-04-22 15:36:20 (delay: 0s)
docker_lamp_1 | Team TestV (2708d27a-7f31-4d90-bd2b-b5bde1c0211e) is not yet assigned an owner. skipping...
docker_lamp_1 | Team KioskAccount (dedab245-604c-4cd2-a83e-4b9e034b2772) is not yet assigned an owner. skipping...
docker_lamp_1 | [HubSpot] Syncing objects for InsightSquared (b2b115eb-93ce-4d1b-929c-173757df8fba) since 2026-02-17 15:09:59 (delay: 0s)
docker_lamp_1 | Team Aircall Demo (7980e5eb-b11c-4cee-9c21-8bb29ba85f3b) is not yet assigned an owner. skipping...
docker_lamp_1 | [HubSpot] Syncing objects for GoStudent UAT (b2d49a54-b645-4637-a7ae-a86cfce6e8e4) since 2026-02-17 15:09:59 (delay: 1s)
docker_lamp_1 | [HubSpot] Syncing objects for JustCall (c6b9d6b0-b48d-4832-a68c-a57d60651888) since 2026-02-17 15:07:41 (delay: 1s)
docker_lamp_1 | Team Twilio Video (c334ca55-b230-411c-b10e-31c8204bd07b) is not yet assigned an owner. skipping...
docker_lamp_1 | Team My Test Account 3000 (dbc9990d-b35f-4e38-9550-22cdd6059514) is not yet assigned an owner. skipping...
docker_lamp_1 | Team test (7997eb70-8aa4-491a-870d-311977568df4) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Test (5e06dcee-0613-470e-9a77-2c283198f3bf) is not yet assigned an owner. skipping...
docker_lamp_1 | Team test ogg auto sync (da44776e-306f-427a-83d8-a1b4baa5537e) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Tourlaner (d9b71080-388b-4cf5-8175-aa0f29bee635) is not yet assigned an owner. skipping...
docker_lamp_1 | Dispatched 4 HubSpot sync jobs
docker_lamp_1 | 2026-04-22 15:41:11 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:sync-hubspot-objects > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | 2026-04-22 15:41:11 Jiminny\Jobs\Crm\SyncHubspotObjects ....... 44.08ms DONE
docker_lamp_1 | 2026-04-22 15:41:11 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 | 2026-04-22 15:41:12 Jiminny\Jobs\Crm\SyncHubspotObjects ...... 577.01ms DONE
docker_lamp_1 | 2026-04-22 15:41:12 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:41:12 Jiminny\Jobs\Crm\SyncHubspotObjects ....... 16.42ms DONE
docker_lamp_1 | 2026-04-22 15:41:12 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:41:12 Jiminny\Jobs\Crm\SyncHubspotObjects ....... 17.23ms DONE
docker_lamp_1 |
docker_lamp_1 | 2026-04-22 15:42:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:42:03 Running ['artisan' dialers:monitor-activities] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:42:05 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:42:06 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:42:07 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:42:09 Running ['artisan' conference:monitor:count] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:42:10 Running ['artisan' mailbox:batch:create] ....... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:create > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 | 2026-04-22 15:42:14 Jiminny\Jobs\Mailbox\CreateBatches [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:42:14 Jiminny\Jobs\Mailbox\CreateBatches [PASSWORD_DOTS] 87.60ms DONE
docker_lamp_1 |
docker_lamp_1 | 2026-04-22 15:43:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:43:04 Running ['artisan' dialers:monitor-activities] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:43:06 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:43:08 Running ['artisan' mailbox:skip-lists:refresh] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:43:10 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:43:11 Running ['artisan' mailbox:batch:retry-failed --max-batches=15] in background 5.15ms DONE
docker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' mailbox:batch:retry-failed --max-batches=15 > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish "framework/schedule-390defd641effba0f73a895e426ded4cf2ba7f11" "$?") > '/dev/null' 2>&1 &
docker_lamp_1 | 2026-04-22 15:43:11 Running ['artisan' calendar:sync --dateMode=daily] 2026-04-22 15:43:19 Jiminny\Jobs\Calendar\SyncCalendarEvents ....... RUNNING
docker_lamp_1 | 2026-04-22 15:43:20 Jiminny\Jobs\Calendar\SyncCalendarEvents ....... 1s DONE
docker_lamp_1 | 2026-04-22 15:43:22 Jiminny\Jobs\Calendar\SyncCalendarEvents ....... RUNNING
docker_lamp_1 | 10s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' calendar:sync --dateMode=daily > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 | 2026-04-22 15:43:23 Jiminny\Jobs\Calendar\SyncCalendarEvents . 596.48ms DONE
docker_lamp_1 | 2026-04-22 15:43:23 Jiminny\Jobs\Calendar\SyncCalendarEvents ....... RUNNING
docker_lamp_1 | 2026-04-22 15:43:23 Jiminny\Jobs\Calendar\SyncCalendarEvents .. 52.11ms DONE
docker_lamp_1 |
docker_lamp_1 | 2026-04-22 15:44:02 Running ['artisan' meeting-bot:schedule-bot] ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:44:04 Running ['artisan' dialers:monitor-activities] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:44:05 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:44:07 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:44:09 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:44:11 Running ['artisan' conference:monitor:count] ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:44:13 Running ['artisan' crm:sync-objects] Syncing objects for Salesforce Team (6473c918-d8db-4ded-a52b-4febfd7b7c02) since 2026-03-19 20:14:10 (delay: 0s)
docker_lamp_1 | Team Edge Communications (c3a725ef-fc5e-4c7b-9231-6379b2834101) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Burlington Textiles Corp of America (72c0ccae-28a5-44bd-a0b4-958e73135463) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Uber (1c523aed-80e3-4c44-b215-7f0b027a03a2) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Vonage (c6030540-121e-4d6b-9261-8d82105817bf) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Nexmo (eaba09e1-4973-4077-b041-ad389248318c) is not yet assigned an owner. skipping...
docker_lamp_1 | Team SF (018e2d7c-ed01-4af2-be33-5c28f454a185) is not yet assigned an owner. skipping...
docker_lamp_1 | Team NewAccount (27325244-25d7-4431-aac4-9ddb2bde6b45) is not yet assigned an owner. skipping...
docker_lamp_1 | Syncing objects for Pipedrive, Inc. (51467630-d89d-480b-be20-933e64a042f7) since 2026-04-08 18:14:13 (delay: 2s)
docker_lamp_1 | Syncing objects for Copper (396ed57c-e3c4-49be-8290-37c32955f7c7) since 2026-04-22 15:14:16 (delay: 4s)
docker_lamp_1 | Syncing objects for Pipedrive External Test (fda3cbdf-1117-4ba5-86f8-775f548b3a28) since 2026-04-08 18:14:16 (delay: 6s)
docker_lamp_1 | Syncing objects for Close (3ff5a02a-86fb-4357-b1d6-a04e26c38602) since 2026-04-22 15:14:20 (delay: 8s)
docker_lamp_1 | Team DeewanyAccount (cfaf90cb-ecf0-432c-a655-a22d9985e4c8) is not yet assigned an owner. skipping...
docker_lamp_1 | Syncing objects for Bullhorn (1640a0ac-19da-4c3b-90f7-87525f07a6d2) since 2026-02-19 13:28:05 (delay: 10s)
docker_lamp_1 | Team Horen test (93d8f8f2-dd61-4137-97de-a02a9d3751bb) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Ahmed Hamadi (191379e4-5e90-4419-8ffd-4804a5abb565) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Ahmed Testing (3b2e1fad-bc5f-4322-84d2-a7440f78c5d7) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Ahmed jiminny (1dbd47b8-bbf8-4bbb-8e28-67b9c91f566e) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Testers Inc (ec0767d5-1248-4447-b316-94dd9a30d52b) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Copper Team (817c8ce1-8e22-4ac8-8fca-8df1131963ab) is not yet assigned an owner. skipping...
docker_lamp_1 | Team TheBestPlace Ever (dbb998f9-70eb-4f11-a9f2-d9f6157f4035) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Horen's apartments (e10bb4ea-6c89-4fe3-be3b-0a278aa81180) is not yet assigned an owner. skipping...
docker_lamp_1 | Team New Org Test (2cb527b6-73a4-46be-b7b2-b0097b494cc5) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Pipedrive test org (18ea22be-6e0b-461c-a4db-536c022aac73) is not yet assigned an owner. skipping...
docker_lamp_1 | Team BigChairs Inc. (16dcfaf9-44b4-4cc2-9ead-f98dd0cad4dd) is not yet assigned an owner. skipping...
docker_lamp_1 | Syncing objects for GL 500 (0c33bf2d-1c77-4200-8ed6-6147ad444c30) since 2026-02-19 13:26:04 (delay: 12s)
docker_lamp_1 | Team Loren X (243d7d4b-1849-46c2-9dfb-4462f52020ef) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Laravel Company 2024 (e466d657-c26d-4281-9ed5-73890fe711f6) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Barbara Fuchs (bea0fcf3-f8f9-471d-92c4-d1b7d1a2e228) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Test salesforce auto sync (2c2e652e-1cbb-4620-852d-e73bef0ab4a9) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Bullhorn (83c14120-cdb1-42e9-b0ac-16ef11320a8f) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Globo Tech (fb8d5211-3c4d-4804-9223-b091670ffd79) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Laravel 11 Company 2024 (780c6836-764b-4e47-9a05-cfefbab6e590) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Nikon7 (e9bff902-edc3-4b0b-89b1-2a9aa43d940a) is not yet assigned an owner. skipping...
docker_lamp_1 | Syncing objects for Dev Zoho CRM client (1ece66c8-feb1-4df1-b321-21607daf4623) since 2026-04-22 15:14:26 (delay: 14s)
docker_lamp_1 | [PASSWORD_DOTS] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:sync-objects > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 | 2026-04-22 15:44:15 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:15 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 49.60ms DONE
docker_lamp_1 | 2026-04-22 15:44:15 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob RUNNING
docker_lamp_1 | 2026-04-22 15:44:16 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob 601.82ms DONE
docker_lamp_1 | 2026-04-22 15:44:17 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:17 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 24.34ms DONE
docker_lamp_1 | 2026-04-22 15:44:19 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:20 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 1s DONE
docker_lamp_1 | 2026-04-22 15:44:20 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob RUNNING
docker_lamp_1 | 2026-04-22 15:44:21 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob 557.11ms DONE
docker_lamp_1 | 2026-04-22 15:44:21 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:21 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 58.10ms DONE
docker_lamp_1 | 2026-04-22 15:44:23 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:24 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 1s DONE
docker_lamp_1 | 2026-04-22 15:44:25 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob RUNNING
docker_lamp_1 | 2026-04-22 15:44:26 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob 787.71ms DONE
docker_lamp_1 | 2026-04-22 15:44:26 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:26 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 18.07ms DONE
docker_lamp_1 | 2026-04-22 15:44:27 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:27 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 19.18ms DONE
docker_lamp_1 | 2026-04-22 15:44:29 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:33 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 3s DONE
docker_lamp_1 | 2026-04-22 15:44:33 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob RUNNING
docker_lamp_1 | 2026-04-22 15:44:34 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob 848.26ms DONE
docker_lamp_1 | 2026-04-22 15:44:35 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob RUNNING
docker_lamp_1 | 2026-04-22 15:44:35 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob 431.28ms DONE
docker_lamp_1 |
docker_lamp_1 | 2026-04-22 15:45:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:03 Running ['artisan' dialers:monitor-activities] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:06 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:09 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:10 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:12 Running ['artisan' activity:purge-stale] ....... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:purge-stale > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:15 Running ['artisan' mailbox:text-relay:sync] {
docker_lamp_1 | "error": "invalid_request",
docker_lamp_1 | "error_description": "Invalid impersonation \u0026quot;sub\u0026quot; field: @"
docker_lamp_1 | }
docker_lamp_1 | .... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:text-relay:sync > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:17 Running ['artisan' conference:pre-meeting-notification] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:pre-meeting-notification > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:19 Running ['artisan' conference:monitor:start] ... 4s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:start > '/proc/1/fd/1' 2>&1
unexpected EOF
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/infrastructure/dev/docker (develop) $ work
WARN[0000] /Users/lukas/jiminny/infrastructure/dev/docker/docker-compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion
Attaching to blackfire-1, datadog-1, jiminny_ext-1, mariadb-1, docker_lamp_1, elasticsearch, kibana, ngrok, redis
redis | 1:C 23 Apr 2026 07:55:51.731 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
redis | 1:C 23 Apr 2026 07:55:51.731 # Redis version=5.0.14, bits=64, commit=00000000, modified=0, pid=1, just started
redis | 1:C 23 Apr 2026 07:55:51.731 # Configuration loaded
mariadb-1 | 2026-04-23 07:55:51+00:00 [Note] [Entrypoint]: Entrypoint script for MariaDB Server 1:11.4.5+maria~ubu2404 started.
redis | 1:M 23 Apr 2026 07:55:51.741 * Running mode=standalone, port=6379.
redis | 1:M 23 Apr 2026 07:55:51.741 # Server initialized
redis | 1:M 23 Apr 2026 07:55:51.741 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled.
redis | 1:M 23 Apr 2026 07:55:51.744 * Reading RDB preamble from AOF file...
redis | 1:M 23 Apr 2026 07:55:51.745 * Reading the remaining AOF tail...
blackfire-1 | [2026-04-23T07:55:51Z] ERROR: The server ID parameter is not set. Please run 'blackfire-agent -register' to configure it.
blackfire-1 | usage blackfire-agent [options]
blackfire-1 | --collector="[URL_WITH_CREDENTIALS] controller (64 bit): Version 7.10.2 (Build 40a3af639d4698) Copyright (c) 2020 Elasticsearch BV" }
elasticsearch | {"type": "server", "timestamp": "2026-04-23T07:56:08,335Z", "level": "INFO", "component": "o.e.t.NettyAllocator", "cluster.name": "docker-cluster", "node.name": "e802ad473a4f", "message": "creating NettyAllocator with the following configs: [name=unpooled, suggested_max_allocation_size=256kb, factors={es.unsafe.use_unpooled_allocator=null, g1gc_enabled=true, g1gc_region_size=1mb, heap_size=700mb}]" }
elasticsearch | {"type": "server", "timestamp": "2026-04-23T07:56:08,444Z", "level": "INFO", "component": "o.e.d.DiscoveryModule", "cluster.name": "docker-cluster", "node.name": "e802ad473a4f", "message": "using discovery type [single-node] and seed hosts providers [settings]" }
elasticsearch | {"type": "server", "timestamp": "2026-04-23T07:56:08,962Z", "level": "WARN", "component": "o.e.g.DanglingIndicesState", "cluster.name": "docker-cluster", "node.name": "e802ad473a4f", "message": "gateway.auto_import_dangling_indices is disabled, dangling indices will not be automatically detected or imported and must be managed manually" }
elasticsearch | {"type": "server", "timestamp": "2026-04-23T07:56:09,262Z", "level": "INFO", "component": "o.e.n.Node", "cluster.name": "docker-cluster", "node.name": "e802ad473a4f", "message": "initialized" }
elasticsearch | {"type": "server", "timestamp": "2026-04-23T07:56:09,263Z", "level": "INFO", "component": "o.e.n.Node", "cluster.name": "docker-cluster", "node.name": "e802ad473a4f", "message": "starting ..." }
elasticsearch | {"type": "server", "timestamp": "2026-04-23T07:56:09,430Z", "level": "INFO", "component": "o.e.t.TransportService", "cluster.name": "docker-cluster", "node.name...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"0 social account(s) to be processed ...\ndocker_lamp_1 | \ndocker_lamp_1 | Done!\ndocker_lamp_1 | 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:bullhorn:ping --heartbeat > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-04-22 15:41:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:41:04 Running ['artisan' dialers:monitor-activities] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:41:06 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:41:07 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:41:08 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:41:10 Running ['artisan' crm:sync-hubspot-objects] [HubSpot] Syncing objects for Hubspot (abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4) since 2026-04-22 15:36:20 (delay: 0s)\ndocker_lamp_1 | Team TestV (2708d27a-7f31-4d90-bd2b-b5bde1c0211e) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team KioskAccount (dedab245-604c-4cd2-a83e-4b9e034b2772) is not yet assigned an owner. skipping...\ndocker_lamp_1 | [HubSpot] Syncing objects for InsightSquared (b2b115eb-93ce-4d1b-929c-173757df8fba) since 2026-02-17 15:09:59 (delay: 0s)\ndocker_lamp_1 | Team Aircall Demo (7980e5eb-b11c-4cee-9c21-8bb29ba85f3b) is not yet assigned an owner. skipping...\ndocker_lamp_1 | [HubSpot] Syncing objects for GoStudent UAT (b2d49a54-b645-4637-a7ae-a86cfce6e8e4) since 2026-02-17 15:09:59 (delay: 1s)\ndocker_lamp_1 | [HubSpot] Syncing objects for JustCall (c6b9d6b0-b48d-4832-a68c-a57d60651888) since 2026-02-17 15:07:41 (delay: 1s)\ndocker_lamp_1 | Team Twilio Video (c334ca55-b230-411c-b10e-31c8204bd07b) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team My Test Account 3000 (dbc9990d-b35f-4e38-9550-22cdd6059514) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team test (7997eb70-8aa4-491a-870d-311977568df4) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Test (5e06dcee-0613-470e-9a77-2c283198f3bf) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team test ogg auto sync (da44776e-306f-427a-83d8-a1b4baa5537e) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Tourlaner (d9b71080-388b-4cf5-8175-aa0f29bee635) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Dispatched 4 HubSpot sync jobs\ndocker_lamp_1 | 2026-04-22 15:41:11 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:sync-hubspot-objects > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | 2026-04-22 15:41:11 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ....... 44.08ms DONE\ndocker_lamp_1 | 2026-04-22 15:41:11 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | 2026-04-22 15:41:12 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ...... 577.01ms DONE\ndocker_lamp_1 | 2026-04-22 15:41:12 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | 2026-04-22 15:41:12 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ....... 16.42ms DONE\ndocker_lamp_1 | 2026-04-22 15:41:12 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | 2026-04-22 15:41:12 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ....... 17.23ms DONE\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-04-22 15:42:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:42:03 Running ['artisan' dialers:monitor-activities] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:42:05 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:42:06 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:42:07 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:42:09 Running ['artisan' conference:monitor:count] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:42:10 Running ['artisan' mailbox:batch:create] ....... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:create > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | 2026-04-22 15:42:14 Jiminny\\Jobs\\Mailbox\\CreateBatches ............. RUNNING\ndocker_lamp_1 | 2026-04-22 15:42:14 Jiminny\\Jobs\\Mailbox\\CreateBatches ........ 87.60ms DONE\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-04-22 15:43:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:43:04 Running ['artisan' dialers:monitor-activities] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:43:06 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:43:08 Running ['artisan' mailbox:skip-lists:refresh] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:43:10 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:43:11 Running ['artisan' mailbox:batch:retry-failed --max-batches=15] in background 5.15ms DONE\ndocker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' mailbox:batch:retry-failed --max-batches=15 > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish \"framework/schedule-390defd641effba0f73a895e426ded4cf2ba7f11\" \"$?\") > '/dev/null' 2>&1 & \ndocker_lamp_1 | 2026-04-22 15:43:11 Running ['artisan' calendar:sync --dateMode=daily] 2026-04-22 15:43:19 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents ....... RUNNING\ndocker_lamp_1 | 2026-04-22 15:43:20 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents ....... 1s DONE\ndocker_lamp_1 | 2026-04-22 15:43:22 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents ....... RUNNING\ndocker_lamp_1 | 10s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' calendar:sync --dateMode=daily > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | 2026-04-22 15:43:23 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents . 596.48ms DONE\ndocker_lamp_1 | 2026-04-22 15:43:23 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents ....... RUNNING\ndocker_lamp_1 | 2026-04-22 15:43:23 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents .. 52.11ms DONE\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-04-22 15:44:02 Running ['artisan' meeting-bot:schedule-bot] ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:44:04 Running ['artisan' dialers:monitor-activities] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:44:05 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:44:07 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:44:09 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:44:11 Running ['artisan' conference:monitor:count] ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:44:13 Running ['artisan' crm:sync-objects] Syncing objects for Salesforce Team (6473c918-d8db-4ded-a52b-4febfd7b7c02) since 2026-03-19 20:14:10 (delay: 0s)\ndocker_lamp_1 | Team Edge Communications (c3a725ef-fc5e-4c7b-9231-6379b2834101) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Burlington Textiles Corp of America (72c0ccae-28a5-44bd-a0b4-958e73135463) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Uber (1c523aed-80e3-4c44-b215-7f0b027a03a2) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Vonage (c6030540-121e-4d6b-9261-8d82105817bf) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Nexmo (eaba09e1-4973-4077-b041-ad389248318c) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team SF (018e2d7c-ed01-4af2-be33-5c28f454a185) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team NewAccount (27325244-25d7-4431-aac4-9ddb2bde6b45) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Syncing objects for Pipedrive, Inc. (51467630-d89d-480b-be20-933e64a042f7) since 2026-04-08 18:14:13 (delay: 2s)\ndocker_lamp_1 | Syncing objects for Copper (396ed57c-e3c4-49be-8290-37c32955f7c7) since 2026-04-22 15:14:16 (delay: 4s)\ndocker_lamp_1 | Syncing objects for Pipedrive External Test (fda3cbdf-1117-4ba5-86f8-775f548b3a28) since 2026-04-08 18:14:16 (delay: 6s)\ndocker_lamp_1 | Syncing objects for Close (3ff5a02a-86fb-4357-b1d6-a04e26c38602) since 2026-04-22 15:14:20 (delay: 8s)\ndocker_lamp_1 | Team DeewanyAccount (cfaf90cb-ecf0-432c-a655-a22d9985e4c8) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Syncing objects for Bullhorn (1640a0ac-19da-4c3b-90f7-87525f07a6d2) since 2026-02-19 13:28:05 (delay: 10s)\ndocker_lamp_1 | Team Horen test (93d8f8f2-dd61-4137-97de-a02a9d3751bb) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Ahmed Hamadi (191379e4-5e90-4419-8ffd-4804a5abb565) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Ahmed Testing (3b2e1fad-bc5f-4322-84d2-a7440f78c5d7) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Ahmed jiminny (1dbd47b8-bbf8-4bbb-8e28-67b9c91f566e) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Testers Inc (ec0767d5-1248-4447-b316-94dd9a30d52b) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Copper Team (817c8ce1-8e22-4ac8-8fca-8df1131963ab) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team TheBestPlace Ever (dbb998f9-70eb-4f11-a9f2-d9f6157f4035) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Horen's apartments (e10bb4ea-6c89-4fe3-be3b-0a278aa81180) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team New Org Test (2cb527b6-73a4-46be-b7b2-b0097b494cc5) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Pipedrive test org (18ea22be-6e0b-461c-a4db-536c022aac73) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team BigChairs Inc. (16dcfaf9-44b4-4cc2-9ead-f98dd0cad4dd) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Syncing objects for GL 500 (0c33bf2d-1c77-4200-8ed6-6147ad444c30) since 2026-02-19 13:26:04 (delay: 12s)\ndocker_lamp_1 | Team Loren X (243d7d4b-1849-46c2-9dfb-4462f52020ef) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Laravel Company 2024 (e466d657-c26d-4281-9ed5-73890fe711f6) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Barbara Fuchs (bea0fcf3-f8f9-471d-92c4-d1b7d1a2e228) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Test salesforce auto sync (2c2e652e-1cbb-4620-852d-e73bef0ab4a9) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Bullhorn (83c14120-cdb1-42e9-b0ac-16ef11320a8f) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Globo Tech (fb8d5211-3c4d-4804-9223-b091670ffd79) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Laravel 11 Company 2024 (780c6836-764b-4e47-9a05-cfefbab6e590) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Nikon7 (e9bff902-edc3-4b0b-89b1-2a9aa43d940a) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Syncing objects for Dev Zoho CRM client (1ece66c8-feb1-4df1-b321-21607daf4623) since 2026-04-22 15:14:26 (delay: 14s)\ndocker_lamp_1 | ........... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:sync-objects > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | 2026-04-22 15:44:15 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:15 Jiminny\\Jobs\\Crm\\SyncObjects .............. 49.60ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:15 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:16 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob 601.82ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:17 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:17 Jiminny\\Jobs\\Crm\\SyncObjects .............. 24.34ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:19 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:20 Jiminny\\Jobs\\Crm\\SyncObjects ................... 1s DONE\ndocker_lamp_1 | 2026-04-22 15:44:20 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:21 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob 557.11ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:21 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:21 Jiminny\\Jobs\\Crm\\SyncObjects .............. 58.10ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:23 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:24 Jiminny\\Jobs\\Crm\\SyncObjects ................... 1s DONE\ndocker_lamp_1 | 2026-04-22 15:44:25 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:26 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob 787.71ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:26 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:26 Jiminny\\Jobs\\Crm\\SyncObjects .............. 18.07ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:27 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:27 Jiminny\\Jobs\\Crm\\SyncObjects .............. 19.18ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:29 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:33 Jiminny\\Jobs\\Crm\\SyncObjects ................... 3s DONE\ndocker_lamp_1 | 2026-04-22 15:44:33 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:34 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob 848.26ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:35 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:35 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob 431.28ms DONE\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-04-22 15:45:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:03 Running ['artisan' dialers:monitor-activities] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:06 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:09 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:10 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:12 Running ['artisan' activity:purge-stale] ....... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:purge-stale > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:15 Running ['artisan' mailbox:text-relay:sync] {\ndocker_lamp_1 | \"error\": \"invalid_request\",\ndocker_lamp_1 | \"error_description\": \"Invalid impersonation \\u0026quot;sub\\u0026quot; field: @\"\ndocker_lamp_1 | }\ndocker_lamp_1 | .... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:text-relay:sync > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:17 Running ['artisan' conference:pre-meeting-notification] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:pre-meeting-notification > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:19 Running ['artisan' conference:monitor:start] ... 4s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:start > '/proc/1/fd/1' 2>&1 \nunexpected EOF\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/infrastructure/dev/docker (develop) $ work\nWARN[0000] /Users/lukas/jiminny/infrastructure/dev/docker/docker-compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion \nAttaching to blackfire-1, datadog-1, jiminny_ext-1, mariadb-1, docker_lamp_1, elasticsearch, kibana, ngrok, redis\nredis | 1:C 23 Apr 2026 07:55:51.731 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo\nredis | 1:C 23 Apr 2026 07:55:51.731 # Redis version=5.0.14, bits=64, commit=00000000, modified=0, pid=1, just started\nredis | 1:C 23 Apr 2026 07:55:51.731 # Configuration loaded\nmariadb-1 | 2026-04-23 07:55:51+00:00 [Note] [Entrypoint]: Entrypoint script for MariaDB Server 1:11.4.5+maria~ubu2404 started.\nredis | 1:M 23 Apr 2026 07:55:51.741 * Running mode=standalone, port=6379.\nredis | 1:M 23 Apr 2026 07:55:51.741 # Server initialized\nredis | 1:M 23 Apr 2026 07:55:51.741 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled.\nredis | 1:M 23 Apr 2026 07:55:51.744 * Reading RDB preamble from AOF file...\nredis | 1:M 23 Apr 2026 07:55:51.745 * Reading the remaining AOF tail...\nblackfire-1 | [2026-04-23T07:55:51Z] ERROR: The server ID parameter is not set. Please run 'blackfire-agent -register' to configure it.\nblackfire-1 | usage blackfire-agent [options]\nblackfire-1 | --collector=\"https://blackfire.io\": Sets the URL of Blackfire's data collector\nblackfire-1 | --config=\"/etc/blackfire/agent\": Sets the path to the configuration file\nblackfire-1 | -d: Prints the current configuration\nblackfire-1 | --http-proxy=\"\": Sets the HTTP proxy to use\nblackfire-1 | --https-proxy=\"\": Sets the HTTPS proxy to use\nblackfire-1 | --log-file=\"stderr\": Sets the path of the log file. Use stderr to log to stderr\nblackfire-1 | --log-level=\"1\": log verbosity level (4: debug, 3: info, 2: warning, 1: error)\nblackfire-1 | --register: Helps you with registering the agent\nblackfire-1 | --server-id=\"\": Sets the server id used to authenticate with Blackfire API\nblackfire-1 | --server-token=\"\": Sets the server token used to authenticate with Blackfire API. It is unsafe to set this from the command line\nblackfire-1 | --socket=\"unix:///var/run/blackfire/agent.sock\": Sets the socket the agent should read traces from. Possible value can be a unix socket or a TCP address. ie: unix:///var/run/blackfire/agent.sock or tcp://127.0.0.1:8307\nblackfire-1 | --test: Tests the configuration\nblackfire-1 | --timeout=\"15s\": Sets the Blackfire connection timeout\nblackfire-1 | -v: Prints the version number\nngrok | t=2026-04-23T07:55:52+0000 lvl=info msg=\"no configuration paths supplied\"\nngrok | t=2026-04-23T07:55:52+0000 lvl=info msg=\"using configuration at default config path\" path=/home/ngrok/.ngrok2/ngrok.yml\nngrok | t=2026-04-23T07:55:52+0000 lvl=info msg=\"open config file\" path=/home/ngrok/.ngrok2/ngrok.yml err=nil\nblackfire-1 exited with code 1\njiminny_ext-1 exited with code 0\nngrok | t=2026-04-23T07:55:52+0000 lvl=info msg=\"starting web service\" obj=web addr=0.0.0.0:4040\ndocker_lamp_1 | + main\ndocker_lamp_1 | + declare START_DIR\ndocker_lamp_1 | +++ realpath /scripts/init-dev\ndocker_lamp_1 | ++ dirname /scripts/init-dev\ndocker_lamp_1 | + START_DIR=/scripts\ndocker_lamp_1 | + readonly START_DIR\ndocker_lamp_1 | + source /scripts/storage_init.sh\ndocker_lamp_1 | ++ set -o errexit\ndocker_lamp_1 | ++ set -o nounset\ndocker_lamp_1 | ++ set -o pipefail\ndocker_lamp_1 | + create_bind_mount\ndocker_lamp_1 | + [[ 0 == \\1 ]]\ndocker_lamp_1 | + configure_xdebug\ndocker_lamp_1 | + j2 /root/.j2_templates/xdebug/xdebug.ini.j2\nmariadb-1 | 2026-04-23 07:55:52+00:00 [Warn] [Entrypoint]: /sys/fs/cgroup///memory.pressure not writable, functionality unavailable to MariaDB\nmariadb-1 | 2026-04-23 07:55:52+00:00 [Note] [Entrypoint]: Switching to dedicated user 'mysql'\nmariadb-1 | 2026-04-23 07:55:52+00:00 [Note] [Entrypoint]: Entrypoint script for MariaDB Server 1:11.4.5+maria~ubu2404 started.\ndatadog-1 | [s6-init] making user provided files available at /var/run/s6/etc...exited 0.\nngrok | t=2026-04-23T07:55:52+0000 lvl=info msg=\"tunnel session started\" obj=tunnels.session\nngrok | t=2026-04-23T07:55:52+0000 lvl=info msg=\"client session established\" obj=csess id=1c8f9eb905ac\nmariadb-1 | 2026-04-23 07:55:53+00:00 [Note] [Entrypoint]: MariaDB upgrade information missing, assuming required\nmariadb-1 | 2026-04-23 07:55:53+00:00 [Note] [Entrypoint]: MariaDB upgrade (mariadb-upgrade or creating healthcheck users) required, but skipped due to $MARIADB_AUTO_UPGRADE setting\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] Starting MariaDB 11.4.5-MariaDB-ubu2404 source revision 0771110266ff5c04216af4bf1243c65f8c67ccf4 server_uid zs02SyAxkg7epm72pZgde8TQHsk= as process 1\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Compressed tables use zlib 1.3\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Number of transaction pools: 1\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Using ARMv8 crc32 + pmull instructions\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Using liburing\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Initializing buffer pool, total size = 128.000MiB, chunk size = 2.000MiB\nngrok | t=2026-04-23T07:55:53+0000 lvl=info msg=\"started tunnel\" obj=tunnels name=\"command_line (http)\" addr=http://lamp:3080 url=http://lukask.ngrok.io\nngrok | t=2026-04-23T07:55:53+0000 lvl=info msg=\"started tunnel\" obj=tunnels name=command_line addr=http://lamp:3080 url=https://lukask.ngrok.io\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Completed initialization of buffer pool\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: File system buffers for log disabled (block size=512 bytes)\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: End of log at LSN=7618260638\ndocker_lamp_1 | + configure_blackfire\ndocker_lamp_1 | + j2 /root/.j2_templates/blackfire/extension.ini.j2\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Opened 3 undo tablespaces\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: 128 rollback segments in 3 undo tablespaces are active.\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Setting file './ibtmp1' size to 12.000MiB. Physically writing the file full; Please wait ...\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: File './ibtmp1' size is now 12.000MiB.\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: log sequence number 7618260638; transaction id 11490938\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] Plugin 'FEEDBACK' is disabled.\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] Plugin 'wsrep-provider' is disabled.\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Loading buffer pool(s) from /var/lib/mysql/ib_buffer_pool\ndocker_lamp_1 | + j2 /root/.j2_templates/blackfire/cli.ini.j2\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Buffer pool(s) load completed at 260423 7:55:53\nngrok | t=2026-04-23T07:55:53+0000 lvl=info msg=\"update available\" obj=updater\ndocker_lamp_1 | + declare EMPTY_DB\ndocker_lamp_1 | + db_is_empty\ndocker_lamp_1 | ++ find /var/lib/mysql/ -maxdepth 1\ndocker_lamp_1 | ++ wc -l\ndocker_lamp_1 | + [[ 11 -lt 5 ]]\ndocker_lamp_1 | + EMPTY_DB=0\ndocker_lamp_1 | + readonly EMPTY_DB\ndocker_lamp_1 | + [[ 0 -eq 1 ]]\ndocker_lamp_1 | + [[ local == \\l\\o\\c\\a\\l ]]\ndocker_lamp_1 | + set_nginx_domain dev.jiminny.com\ndocker_lamp_1 | + declare -r DOMAIN_NAME=dev.jiminny.com\ndocker_lamp_1 | + cp -f /etc/nginx/nginx_template.conf /etc/nginx/nginx.conf\ndocker_lamp_1 | + sed -i -E s~_JIMINNY_DOMAIN~app.dev.jiminny.com~g /etc/nginx/nginx.conf\ndocker_lamp_1 | + sed -i -E s~_JIMINNY_EXT_DOMAIN~ext.dev.jiminny.com~g /etc/nginx/nginx.conf\ndocker_lamp_1 | + sed -i -E s~_JIMINNY_WEB_DOMAIN~www.dev.jiminny.com~g /etc/nginx/nginx.conf\ndocker_lamp_1 | + [[ -n 3399 ]]\ndocker_lamp_1 | + sed -i -E 's~http:\\/\\/jiminny_ext:8080~http://jiminny_ext:3399~g' /etc/nginx/nginx.conf\ndocker_lamp_1 | + [[ -n host.docker.internal ]]\ndocker_lamp_1 | + sed -i -E 's~http:\\/\\/jiminny_ext~http://host.docker.internal~g' /etc/nginx/nginx.conf\ndocker_lamp_1 | + build_dev\ndocker_lamp_1 | + cd /home/jiminny/\ndocker_lamp_1 | + create_dot_env_local_file\ndocker_lamp_1 | + cp -f /home/jiminny/.env.local /home/jiminny/.env.local.bak\ndocker_lamp_1 | + create_dot_env\ndocker_lamp_1 | + [[ -f /home/jiminny/.env ]]\ndocker_lamp_1 | + return\ndocker_lamp_1 | + declare DB_ADMIN_PASSWORD\ndocker_lamp_1 | + declare DB_ADMIN_USERNAME\ndocker_lamp_1 | + declare DB_DEV_PASSWORD\ndocker_lamp_1 | + declare DB_DEV_USERNAME\ndocker_lamp_1 | + declare DB_ROOT_PASSWORD\ndocker_lamp_1 | + declare DB_ROOT_USERNAME\ndocker_lamp_1 | + declare DB_WEB_PASSWORD\ndocker_lamp_1 | + declare DB_WEB_USERNAME\ndocker_lamp_1 | ++ jq -r .DB_ADMIN_PASSWORD /home/jiminny/dev.json\ndatadog-1 | [s6-init] ensuring user provided files have correct perms...exited 0.\ndocker_lamp_1 | + DB_ADMIN_PASSWORD='dgyt$rTe21-d'\ndocker_lamp_1 | ++ jq -r .DB_ADMIN_USERNAME /home/jiminny/dev.json\ndatadog-1 | [fix-attrs.d] applying ownership & permissions fixes...\ndocker_lamp_1 | + DB_ADMIN_USERNAME=jmnyadmin\ndocker_lamp_1 | ++ jq -r .DB_DEV_PASSWORD /home/jiminny/dev.json\ndatadog-1 | [fix-attrs.d] done.\ndocker_lamp_1 | + DB_DEV_PASSWORD=rTr4sdQA65-Ad\n\n\ndatadog-1 | [cont-init.d] executing container initialization scripts...\ndocker_lamp_1 | ++ jq -r .DB_DEV_USERNAME /home/jiminny/dev.json\ndocker_lamp_1 | + DB_DEV_USERNAME=jmnydev\ndocker_lamp_1 | ++ jq -r .DB_ROOT_PASSWORD /home/jiminny/dev.json\ndatadog-1 | [cont-init.d] 01-check-apikey.sh: executing... \ndocker_lamp_1 | + DB_ROOT_PASSWORD=b7h5-1fH3e54J\ndocker_lamp_1 | ++ jq -r .DB_ROOT_USERNAME /home/jiminny/dev.json\ndocker_lamp_1 | + DB_ROOT_USERNAME=root\ndocker_lamp_1 | ++ jq -r .DB_WEB_PASSWORD /home/jiminny/dev.json\ndatadog-1 | \ndatadog-1 | ==================================================================================\ndatadog-1 | You must set an DD_API_KEY environment variable to run the Datadog Agent container\ndatadog-1 | ==================================================================================\ndatadog-1 | \ndatadog-1 | [cont-init.d] 01-check-apikey.sh: exited 1.\ndocker_lamp_1 | + DB_WEB_PASSWORD=aR5-EWf23b8da\ndocker_lamp_1 | ++ jq -r .DB_WEB_USERNAME /home/jiminny/dev.json\ndocker_lamp_1 | + DB_WEB_USERNAME=jmnyweb\ndocker_lamp_1 | + readonly DB_ADMIN_PASSWORD\ndocker_lamp_1 | + readonly DB_ADMIN_USERNAME\ndocker_lamp_1 | + readonly DB_DEV_PASSWORD\ndocker_lamp_1 | + readonly DB_DEV_USERNAME\ndocker_lamp_1 | + readonly DB_ROOT_PASSWORD\ndocker_lamp_1 | + readonly DB_ROOT_USERNAME\ndocker_lamp_1 | + readonly DB_WEB_PASSWORD\ndocker_lamp_1 | + readonly DB_WEB_USERNAME\ndocker_lamp_1 | + sed -i -E 's~DB_USERNAME=.*$~DB_USERNAME=jmnyadmin~g' /home/jiminny/.env\ndocker_lamp_1 | + sed -i -E 's~DB_PASSWORD=.*$~DB_PASSWORD=dgyt$rTe21-d~g' /home/jiminny/.env\ndocker_lamp_1 | + sed -i -E 's~DB_HOST=.*$~DB_HOST=mariadb~g' /home/jiminny/.env\ndocker_lamp_1 | + cp -f /home/jiminny/.env /home/jiminny/.env.root\ndocker_lamp_1 | + cp -f /home/jiminny/.env /home/jiminny/.env.migrate\ndocker_lamp_1 | + sed -i -E 's~DB_PASSWORD=.*$~DB_PASSWORD=b7h5-1fH3e54J~g' /home/jiminny/.env.migrate\ndocker_lamp_1 | + sed -i -E 's~DB_PASSWORD=.*$~DB_PASSWORD=b7h5-1fH3e54J~g' /home/jiminny/.env.root\ndocker_lamp_1 | + sed -i -E 's~DB_USERNAME=.*$~DB_USERNAME=root~g' /home/jiminny/.env.migrate\ndocker_lamp_1 | + sed -i -E 's~DB_USERNAME=.*$~DB_USERNAME=root~g' /home/jiminny/.env.root\ndocker_lamp_1 | + cp -f /home/jiminny/.env /home/jiminny/.env.local\ndocker_lamp_1 | + echo ''\ndocker_lamp_1 | + echo 'DB_ADMIN_PASSWORD=dgyt$rTe21-d'\ndocker_lamp_1 | + echo DB_ADMIN_USERNAME=jmnyadmin\ndocker_lamp_1 | + echo DB_DEV_PASSWORD=rTr4sdQA65-Ad\ndocker_lamp_1 | + echo DB_DEV_USERNAME=jmnydev\ndocker_lamp_1 | + echo DB_ROOT_PASSWORD=b7h5-1fH3e54J\ndocker_lamp_1 | + echo DB_ROOT_USERNAME=root\ndocker_lamp_1 | + echo DB_WEB_PASSWORD=aR5-EWf23b8da\ndocker_lamp_1 | + echo DB_WEB_USERNAME=jmnyweb\ndocker_lamp_1 | + [[ false == \\f\\a\\l\\s\\e ]]\ndocker_lamp_1 | + declare COMPOSER_PARAM=--prefer-dist\ndocker_lamp_1 | + [[ 0 -eq 1 ]]\ndocker_lamp_1 | + composer install --prefer-dist\ndatadog-1 exited with code 1\nmariadb-1 | 2026-04-23 7:55:54 0 [Note] Server socket created on IP: '0.0.0.0'.\nmariadb-1 | 2026-04-23 7:55:54 0 [Note] Server socket created on IP: '::'.\nmariadb-1 | 2026-04-23 7:55:54 0 [Note] mariadbd: Event Scheduler: Loaded 0 events\nmariadb-1 | 2026-04-23 7:55:54 0 [Note] mariadbd: ready for connections.\nmariadb-1 | Version: '11.4.5-MariaDB-ubu2404' socket: '/run/mysqld/mysqld.sock' port: 3306 mariadb.org binary distribution\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:56,514Z\", \"level\": \"INFO\", \"component\": \"o.e.n.Node\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"version[7.10.2], pid[7], build[default/docker/747e1cc71def077253878a59143c1f785afa92b9/2021-01-13T04:42:47.157277Z], OS[Linux/6.12.54-linuxkit/aarch64], JVM[AdoptOpenJDK/OpenJDK 64-Bit Server VM/15.0.1/15.0.1+9]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:56,517Z\", \"level\": \"INFO\", \"component\": \"o.e.n.Node\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"JVM home [/usr/share/elasticsearch/jdk], using bundled JDK [true]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:56,518Z\", \"level\": \"INFO\", \"component\": \"o.e.n.Node\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"JVM arguments [-Xshare:auto, -Des.networkaddress.cache.ttl=60, -Des.networkaddress.cache.negative.ttl=10, -XX:+AlwaysPreTouch, -Xss1m, -Djava.awt.headless=true, -Dfile.encoding=UTF-8, -Djna.nosys=true, -XX:-OmitStackTraceInFastThrow, -XX:+ShowCodeDetailsInExceptionMessages, -Dio.netty.noUnsafe=true, -Dio.netty.noKeySetOptimization=true, -Dio.netty.recycler.maxCapacityPerThread=0, -Dio.netty.allocator.numDirectArenas=0, -Dlog4j.shutdownHookEnabled=false, -Dlog4j2.disable.jmx=true, -Djava.locale.providers=SPI,COMPAT, -Xms1g, -Xmx1g, -XX:+UseG1GC, -XX:G1ReservePercent=25, -XX:InitiatingHeapOccupancyPercent=30, -Djava.io.tmpdir=/tmp/elasticsearch-10124985765930456581, -XX:+HeapDumpOnOutOfMemoryError, -XX:HeapDumpPath=data, -XX:ErrorFile=logs/hs_err_pid%p.log, -Xlog:gc*,gc+age=trace,safepoint:file=logs/gc.log:utctime,pid,tags:filecount=32,filesize=64m, -Des.cgroups.hierarchy.override=/, -Xms700m, -Xmx700m, -XX:MaxDirectMemorySize=367001600, -Des.path.home=/usr/share/elasticsearch, -Des.path.conf=/usr/share/elasticsearch/config, -Des.distribution.flavor=default, -Des.distribution.type=docker, -Des.bundled_jdk=true]\" }\ndocker_lamp_1 | Installing dependencies from lock file (including require-dev)\ndocker_lamp_1 | Verifying lock file contents can be installed on current platform.\ndocker_lamp_1 | Nothing to install, update or remove\ndocker_lamp_1 | Package doctrine/annotations is abandoned, you should avoid using it. No replacement was suggested.\ndocker_lamp_1 | Package symfony/debug is abandoned, you should avoid using it. Use symfony/error-handler instead.\ndocker_lamp_1 | Generating optimized autoload files\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,062Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [aggs-matrix-stats]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,062Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [analysis-common]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,063Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [constant-keyword]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,063Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [flattened]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,063Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [frozen-indices]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,063Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [ingest-common]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,063Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [ingest-geoip]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,063Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [ingest-user-agent]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,063Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [kibana]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,064Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [lang-expression]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,064Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [lang-mustache]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [lang-painless]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [mapper-extras]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [mapper-version]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [parent-join]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [percolator]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [rank-eval]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [reindex]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [repositories-metering-api]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [repository-url]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [search-business-rules]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [searchable-snapshots]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [spatial]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [transform]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [transport-netty4]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [unsigned-long]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [vectors]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [wildcard]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-analytics]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-async]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-async-search]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-autoscaling]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-ccr]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-core]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-data-streams]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-deprecation]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-enrich]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-eql]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-graph]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-identity-provider]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-ilm]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-logstash]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-ml]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-monitoring]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-ql]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-rollup]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-security]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-sql]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,068Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-stack]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,068Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-voting-only-node]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,068Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-watcher]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,068Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"no plugins loaded\" }\nelasticsearch | {\"type\": \"deprecation\", \"timestamp\": \"2026-04-23T07:55:59,122Z\", \"level\": \"DEPRECATION\", \"component\": \"o.e.d.c.s.Settings\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"[node.data] setting was deprecated in Elasticsearch and will be removed in a future release! See the breaking changes documentation for the next major version.\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,139Z\", \"level\": \"INFO\", \"component\": \"o.e.e.NodeEnvironment\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"using [1] data paths, mounts [[/usr/share/elasticsearch/data (/dev/vda1)]], net usable_space [14.5gb], net total_space [58.3gb], types [ext4]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,139Z\", \"level\": \"INFO\", \"component\": \"o.e.e.NodeEnvironment\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"heap size [700mb], compressed ordinary object pointers [true]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,263Z\", \"level\": \"INFO\", \"component\": \"o.e.n.Node\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"node name [e802ad473a4f], node ID [e2ZKzgw4Q4aCf2w5ljWr1A], cluster name [docker-cluster], roles [transform, master, remote_cluster_client, data, ml, data_content, data_hot, data_warm, data_cold, ingest]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:06,887Z\", \"level\": \"INFO\", \"component\": \"o.e.x.m.p.l.CppLogMessageHandler\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"[controller/214] [Main.cc@114] controller (64 bit): Version 7.10.2 (Build 40a3af639d4698) Copyright (c) 2020 Elasticsearch BV\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:08,335Z\", \"level\": \"INFO\", \"component\": \"o.e.t.NettyAllocator\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"creating NettyAllocator with the following configs: [name=unpooled, suggested_max_allocation_size=256kb, factors={es.unsafe.use_unpooled_allocator=null, g1gc_enabled=true, g1gc_region_size=1mb, heap_size=700mb}]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:08,444Z\", \"level\": \"INFO\", \"component\": \"o.e.d.DiscoveryModule\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"using discovery type [single-node] and seed hosts providers [settings]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:08,962Z\", \"level\": \"WARN\", \"component\": \"o.e.g.DanglingIndicesState\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"gateway.auto_import_dangling_indices is disabled, dangling indices will not be automatically detected or imported and must be managed manually\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:09,262Z\", \"level\": \"INFO\", \"component\": \"o.e.n.Node\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"initialized\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:09,263Z\", \"level\": \"INFO\", \"component\": \"o.e.n.Node\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"starting ...\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:09,430Z\", \"level\": \"INFO\", \"component\": \"o.e.t.TransportService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"publish_address {172.18.0.2:9300}, bound_addresses {[::]:9300}\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:09,769Z\", \"level\": \"INFO\", \"component\": \"o.e.c.c.Coordinator\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"cluster UUID [8uh2w1CUSGyWYR_OvaKx6g]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:09,886Z\", \"level\": \"INFO\", \"component\": \"o.e.c.s.MasterService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"elected-as-master ([1] nodes joined)[{e802ad473a4f}{e2ZKzgw4Q4aCf2w5ljWr1A}{cv7PSrNNRgOBYzeY8N23MA}{172.18.0.2}{172.18.0.2:9300}{cdhilmrstw}{ml.machine_memory=4109217792, xpack.installed=true, transform.node=true, ml.max_open_jobs=20} elect leader, _BECOME_MASTER_TASK_, _FINISH_ELECTION_], term: 229, version: 8527, delta: master node changed {previous [], current [{e802ad473a4f}{e2ZKzgw4Q4aCf2w5ljWr1A}{cv7PSrNNRgOBYzeY8N23MA}{172.18.0.2}{172.18.0.2:9300}{cdhilmrstw}{ml.machine_memory=4109217792, xpack.installed=true, transform.node=true, ml.max_open_jobs=20}]}\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:09,973Z\", \"level\": \"INFO\", \"component\": \"o.e.c.s.ClusterApplierService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"master node changed {previous [], current [{e802ad473a4f}{e2ZKzgw4Q4aCf2w5ljWr1A}{cv7PSrNNRgOBYzeY8N23MA}{172.18.0.2}{172.18.0.2:9300}{cdhilmrstw}{ml.machine_memory=4109217792, xpack.installed=true, transform.node=true, ml.max_open_jobs=20}]}, term: 229, version: 8527, reason: Publication{term=229, version=8527}\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:10,019Z\", \"level\": \"INFO\", \"component\": \"o.e.h.AbstractHttpServerTransport\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"publish_address {172.18.0.2:9200}, bound_addresses {[::]:9200}\", \"cluster.uuid\": \"8uh2w1CUSGyWYR_OvaKx6g\", \"node.id\": \"e2ZKzgw4Q4aCf2w5ljWr1A\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:10,020Z\", \"level\": \"INFO\", \"component\": \"o.e.n.Node\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"started\", \"cluster.uuid\": \"8uh2w1CUSGyWYR_OvaKx6g\", \"node.id\": \"e2ZKzgw4Q4aCf2w5ljWr1A\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:10,388Z\", \"level\": \"INFO\", \"component\": \"o.e.l.LicenseService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"license [85e882e5-5714-4173-a5dd-9baa841494a0] mode [basic] - valid\", \"cluster.uuid\": \"8uh2w1CUSGyWYR_OvaKx6g\", \"node.id\": \"e2ZKzgw4Q4aCf2w5ljWr1A\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:10,396Z\", \"level\": \"INFO\", \"component\": \"o.e.g.GatewayService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"recovered [15] indices into cluster_state\", \"cluster.uuid\": \"8uh2w1CUSGyWYR_OvaKx6g\", \"node.id\": \"e2ZKzgw4Q4aCf2w5ljWr1A\" }\nredis | 1:M 23 Apr 2026 07:56:12.207 * DB loaded from append only file: 20.465 seconds\nredis | 1:M 23 Apr 2026 07:56:12.208 * Ready to accept connections\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:12,454Z\", \"level\": \"INFO\", \"component\": \"o.e.c.r.a.AllocationService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"Cluster health status changed from [RED] to [YELLOW] (reason: [shards started [[activities][2]]]).\", \"cluster.uuid\": \"8uh2w1CUSGyWYR_OvaKx6g\", \"node.id\": \"e2ZKzgw4Q4aCf2w5ljWr1A\" }\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:26Z\",\"tags\":[\"info\",\"plugins-service\"],\"pid\":7,\"message\":\"Plugin \\\"visTypeXy\\\" is disabled.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:26Z\",\"tags\":[\"info\",\"plugins-service\"],\"pid\":7,\"message\":\"Plugin \\\"auditTrail\\\" is disabled.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:26Z\",\"tags\":[\"warning\",\"config\",\"deprecation\"],\"pid\":7,\"message\":\"Config key [monitoring.cluster_alerts.email_notifications.email_address] will be required for email notifications to work in 8.0.\\\"\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:26Z\",\"tags\":[\"info\",\"plugins-system\"],\"pid\":7,\"message\":\"Setting up [96] plugins: [taskManager,licensing,globalSearch,globalSearchProviders,code,usageCollection,xpackLegacy,telemetryCollectionManager,telemetry,telemetryCollectionXpack,kibanaUsageCollection,securityOss,newsfeed,mapsLegacy,kibanaLegacy,translations,share,legacyExport,embeddable,uiActionsEnhanced,expressions,data,home,observability,cloud,console,consoleExtensions,apmOss,searchprofiler,painlessLab,grokdebugger,management,indexPatternManagement,advancedSettings,fileUpload,savedObjects,dashboard,visualizations,visTypeTable,visTypeVega,visTypeTimelion,timelion,features,upgradeAssistant,security,snapshotRestore,enterpriseSearch,encryptedSavedObjects,ingestManager,indexManagement,remoteClusters,crossClusterReplication,indexLifecycleManagement,dashboardMode,beatsManagement,transform,ingestPipelines,maps,licenseManagement,graph,dataEnhanced,visTypeMarkdown,tileMap,regionMap,inputControlVis,visualize,esUiShared,charts,lens,visTypeVislib,visTypeTimeseries,rollup,visTypeTagcloud,visTypeMetric,watcher,discover,discoverEnhanced,savedObjectsManagement,spaces,reporting,lists,eventLog,actions,case,alerts,stackAlerts,triggersActionsUi,ml,securitySolution,infra,monitoring,logstash,apm,uptime,bfetch,canvas]\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"security\",\"config\"],\"pid\":7,\"message\":\"Generating a random key for xpack.security.encryptionKey. To prevent sessions from being invalidated on restart, please set xpack.security.encryptionKey in kibana.yml\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"security\",\"config\"],\"pid\":7,\"message\":\"Session cookies will be transmitted over insecure connections. This is not recommended.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"encryptedSavedObjects\",\"config\"],\"pid\":7,\"message\":\"Generating a random key for xpack.encryptedSavedObjects.encryptionKey. To be able to decrypt encrypted saved objects attributes after restart, please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"ingestManager\"],\"pid\":7,\"message\":\"Fleet APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"reporting\",\"config\"],\"pid\":7,\"message\":\"Generating a random key for xpack.reporting.encryptionKey. To prevent sessions from being invalidated on restart, please set xpack.reporting.encryptionKey in kibana.yml\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"reporting\",\"config\"],\"pid\":7,\"message\":\"Found 'server.host: \\\"0\\\"' in Kibana configuration. This is incompatible with Reporting. To enable Reporting to work, 'xpack.reporting.kibanaServer.hostname: 0.0.0.0' is being automatically to the configuration. You can change the setting to 'server.host: 0.0.0.0' or add 'xpack.reporting.kibanaServer.hostname: 0.0.0.0' in kibana.yml to prevent this message.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"reporting\",\"config\"],\"pid\":7,\"message\":\"Chromium sandbox provides an additional layer of protection, but is not supported for Linux CentOS 8.3.2011\\n OS. Automatically setting 'xpack.reporting.capture.browser.chromium.disableSandbox: true'.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"actions\",\"actions\"],\"pid\":7,\"message\":\"APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"alerts\",\"plugins\",\"alerting\"],\"pid\":7,\"message\":\"APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"info\",\"plugins\",\"monitoring\",\"monitoring\"],\"pid\":7,\"message\":\"config sourced from: production cluster\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"info\",\"savedobjects-service\"],\"pid\":7,\"message\":\"Waiting until all Elasticsearch nodes are compatible with Kibana before starting saved objects migrations...\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:28Z\",\"tags\":[\"info\",\"savedobjects-service\"],\"pid\":7,\"message\":\"Starting saved objects migrations\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:28Z\",\"tags\":[\"info\",\"plugins-system\"],\"pid\":7,\"message\":\"Starting [96] plugins: [taskManager,licensing,globalSearch,globalSearchProviders,code,usageCollection,xpackLegacy,telemetryCollectionManager,telemetry,telemetryCollectionXpack,kibanaUsageCollection,securityOss,newsfeed,mapsLegacy,kibanaLegacy,translations,share,legacyExport,embeddable,uiActionsEnhanced,expressions,data,home,observability,cloud,console,consoleExtensions,apmOss,searchprofiler,painlessLab,grokdebugger,management,indexPatternManagement,advancedSettings,fileUpload,savedObjects,dashboard,visualizations,visTypeTable,visTypeVega,visTypeTimelion,timelion,features,upgradeAssistant,security,snapshotRestore,enterpriseSearch,encryptedSavedObjects,ingestManager,indexManagement,remoteClusters,crossClusterReplication,indexLifecycleManagement,dashboardMode,beatsManagement,transform,ingestPipelines,maps,licenseManagement,graph,dataEnhanced,visTypeMarkdown,tileMap,regionMap,inputControlVis,visualize,esUiShared,charts,lens,visTypeVislib,visTypeTimeseries,rollup,visTypeTagcloud,visTypeMetric,watcher,discover,discoverEnhanced,savedObjectsManagement,spaces,reporting,lists,eventLog,actions,case,alerts,stackAlerts,triggersActionsUi,ml,securitySolution,infra,monitoring,logstash,apm,uptime,bfetch,canvas]\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:28Z\",\"tags\":[\"info\",\"plugins\",\"taskManager\",\"taskManager\"],\"pid\":7,\"message\":\"TaskManager is identified by the Kibana UUID: bf01f365-e094-4cde-940d-3e0db65fa22a\"}\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:28,778Z\", \"level\": \"INFO\", \"component\": \"o.e.c.m.MetadataIndexTemplateService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"adding template [.management-beats] for index patterns [.management-beats]\", \"cluster.uuid\": \"8uh2w1CUSGyWYR_OvaKx6g\", \"node.id\": \"e2ZKzgw4Q4aCf2w5ljWr1A\" }\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:28Z\",\"tags\":[\"info\",\"plugins\",\"crossClusterReplication\"],\"pid\":7,\"message\":\"Your basic license does not support crossClusterReplication. Please upgrade your license.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:28Z\",\"tags\":[\"info\",\"plugins\",\"watcher\"],\"pid\":7,\"message\":\"Your basic license does not support watcher. Please upgrade your license.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:28Z\",\"tags\":[\"info\",\"plugins\",\"monitoring\",\"monitoring\",\"kibana-monitoring\"],\"pid\":7,\"message\":\"Starting monitoring stats collection\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:29Z\",\"tags\":[\"error\",\"elasticsearch\",\"data\"],\"pid\":7,\"message\":\"[version_conflict_engine_exception]: [task:Lens-lens_telemetry]: version conflict, document already exists (current version [745])\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:29Z\",\"tags\":[\"error\",\"elasticsearch\",\"data\"],\"pid\":7,\"message\":\"[version_conflict_engine_exception]: [task:Actions-actions_telemetry]: version conflict, document already exists (current version [745])\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:29Z\",\"tags\":[\"error\",\"elasticsearch\",\"data\"],\"pid\":7,\"message\":\"[version_conflict_engine_exception]: [task:endpoint:user-artifact-packager:1.0.0]: version conflict, document already exists (current version [310913])\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:29Z\",\"tags\":[\"error\",\"elasticsearch\",\"data\"],\"pid\":7,\"message\":\"[version_conflict_engine_exception]: [task:Alerting-alerting_telemetry]: version conflict, document already exists (current version [745])\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:29Z\",\"tags\":[\"error\",\"elasticsearch\",\"data\"],\"pid\":7,\"message\":\"[version_conflict_engine_exception]: [task:apm-telemetry-task]: version conflict, document already exists (current version [1222])\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:29Z\",\"tags\":[\"listening\",\"info\"],\"pid\":7,\"message\":\"Server running at http://0:5601\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:31Z\",\"tags\":[\"info\",\"http\",\"server\",\"Kibana\"],\"pid\":7,\"message\":\"http server running at http://0:5601\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:32Z\",\"tags\":[\"warning\",\"plugins\",\"reporting\"],\"pid\":7,\"message\":\"Enabling the Chromium sandbox provides an additional layer of protection.\"}\n\n\nv View in Docker Desktop o View Config w Enable Watch","depth":4,"value":"0 social account(s) to be processed ...\ndocker_lamp_1 | \ndocker_lamp_1 | Done!\ndocker_lamp_1 | 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:bullhorn:ping --heartbeat > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-04-22 15:41:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:41:04 Running ['artisan' dialers:monitor-activities] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:41:06 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:41:07 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:41:08 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:41:10 Running ['artisan' crm:sync-hubspot-objects] [HubSpot] Syncing objects for Hubspot (abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4) since 2026-04-22 15:36:20 (delay: 0s)\ndocker_lamp_1 | Team TestV (2708d27a-7f31-4d90-bd2b-b5bde1c0211e) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team KioskAccount (dedab245-604c-4cd2-a83e-4b9e034b2772) is not yet assigned an owner. skipping...\ndocker_lamp_1 | [HubSpot] Syncing objects for InsightSquared (b2b115eb-93ce-4d1b-929c-173757df8fba) since 2026-02-17 15:09:59 (delay: 0s)\ndocker_lamp_1 | Team Aircall Demo (7980e5eb-b11c-4cee-9c21-8bb29ba85f3b) is not yet assigned an owner. skipping...\ndocker_lamp_1 | [HubSpot] Syncing objects for GoStudent UAT (b2d49a54-b645-4637-a7ae-a86cfce6e8e4) since 2026-02-17 15:09:59 (delay: 1s)\ndocker_lamp_1 | [HubSpot] Syncing objects for JustCall (c6b9d6b0-b48d-4832-a68c-a57d60651888) since 2026-02-17 15:07:41 (delay: 1s)\ndocker_lamp_1 | Team Twilio Video (c334ca55-b230-411c-b10e-31c8204bd07b) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team My Test Account 3000 (dbc9990d-b35f-4e38-9550-22cdd6059514) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team test (7997eb70-8aa4-491a-870d-311977568df4) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Test (5e06dcee-0613-470e-9a77-2c283198f3bf) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team test ogg auto sync (da44776e-306f-427a-83d8-a1b4baa5537e) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Tourlaner (d9b71080-388b-4cf5-8175-aa0f29bee635) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Dispatched 4 HubSpot sync jobs\ndocker_lamp_1 | 2026-04-22 15:41:11 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:sync-hubspot-objects > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | 2026-04-22 15:41:11 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ....... 44.08ms DONE\ndocker_lamp_1 | 2026-04-22 15:41:11 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | 2026-04-22 15:41:12 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ...... 577.01ms DONE\ndocker_lamp_1 | 2026-04-22 15:41:12 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | 2026-04-22 15:41:12 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ....... 16.42ms DONE\ndocker_lamp_1 | 2026-04-22 15:41:12 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | 2026-04-22 15:41:12 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ....... 17.23ms DONE\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-04-22 15:42:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:42:03 Running ['artisan' dialers:monitor-activities] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:42:05 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:42:06 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:42:07 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:42:09 Running ['artisan' conference:monitor:count] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:42:10 Running ['artisan' mailbox:batch:create] ....... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:create > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | 2026-04-22 15:42:14 Jiminny\\Jobs\\Mailbox\\CreateBatches ............. RUNNING\ndocker_lamp_1 | 2026-04-22 15:42:14 Jiminny\\Jobs\\Mailbox\\CreateBatches ........ 87.60ms DONE\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-04-22 15:43:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:43:04 Running ['artisan' dialers:monitor-activities] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:43:06 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:43:08 Running ['artisan' mailbox:skip-lists:refresh] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:43:10 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:43:11 Running ['artisan' mailbox:batch:retry-failed --max-batches=15] in background 5.15ms DONE\ndocker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' mailbox:batch:retry-failed --max-batches=15 > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish \"framework/schedule-390defd641effba0f73a895e426ded4cf2ba7f11\" \"$?\") > '/dev/null' 2>&1 & \ndocker_lamp_1 | 2026-04-22 15:43:11 Running ['artisan' calendar:sync --dateMode=daily] 2026-04-22 15:43:19 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents ....... RUNNING\ndocker_lamp_1 | 2026-04-22 15:43:20 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents ....... 1s DONE\ndocker_lamp_1 | 2026-04-22 15:43:22 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents ....... RUNNING\ndocker_lamp_1 | 10s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' calendar:sync --dateMode=daily > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | 2026-04-22 15:43:23 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents . 596.48ms DONE\ndocker_lamp_1 | 2026-04-22 15:43:23 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents ....... RUNNING\ndocker_lamp_1 | 2026-04-22 15:43:23 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents .. 52.11ms DONE\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-04-22 15:44:02 Running ['artisan' meeting-bot:schedule-bot] ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:44:04 Running ['artisan' dialers:monitor-activities] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:44:05 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:44:07 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:44:09 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:44:11 Running ['artisan' conference:monitor:count] ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:44:13 Running ['artisan' crm:sync-objects] Syncing objects for Salesforce Team (6473c918-d8db-4ded-a52b-4febfd7b7c02) since 2026-03-19 20:14:10 (delay: 0s)\ndocker_lamp_1 | Team Edge Communications (c3a725ef-fc5e-4c7b-9231-6379b2834101) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Burlington Textiles Corp of America (72c0ccae-28a5-44bd-a0b4-958e73135463) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Uber (1c523aed-80e3-4c44-b215-7f0b027a03a2) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Vonage (c6030540-121e-4d6b-9261-8d82105817bf) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Nexmo (eaba09e1-4973-4077-b041-ad389248318c) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team SF (018e2d7c-ed01-4af2-be33-5c28f454a185) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team NewAccount (27325244-25d7-4431-aac4-9ddb2bde6b45) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Syncing objects for Pipedrive, Inc. (51467630-d89d-480b-be20-933e64a042f7) since 2026-04-08 18:14:13 (delay: 2s)\ndocker_lamp_1 | Syncing objects for Copper (396ed57c-e3c4-49be-8290-37c32955f7c7) since 2026-04-22 15:14:16 (delay: 4s)\ndocker_lamp_1 | Syncing objects for Pipedrive External Test (fda3cbdf-1117-4ba5-86f8-775f548b3a28) since 2026-04-08 18:14:16 (delay: 6s)\ndocker_lamp_1 | Syncing objects for Close (3ff5a02a-86fb-4357-b1d6-a04e26c38602) since 2026-04-22 15:14:20 (delay: 8s)\ndocker_lamp_1 | Team DeewanyAccount (cfaf90cb-ecf0-432c-a655-a22d9985e4c8) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Syncing objects for Bullhorn (1640a0ac-19da-4c3b-90f7-87525f07a6d2) since 2026-02-19 13:28:05 (delay: 10s)\ndocker_lamp_1 | Team Horen test (93d8f8f2-dd61-4137-97de-a02a9d3751bb) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Ahmed Hamadi (191379e4-5e90-4419-8ffd-4804a5abb565) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Ahmed Testing (3b2e1fad-bc5f-4322-84d2-a7440f78c5d7) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Ahmed jiminny (1dbd47b8-bbf8-4bbb-8e28-67b9c91f566e) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Testers Inc (ec0767d5-1248-4447-b316-94dd9a30d52b) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Copper Team (817c8ce1-8e22-4ac8-8fca-8df1131963ab) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team TheBestPlace Ever (dbb998f9-70eb-4f11-a9f2-d9f6157f4035) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Horen's apartments (e10bb4ea-6c89-4fe3-be3b-0a278aa81180) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team New Org Test (2cb527b6-73a4-46be-b7b2-b0097b494cc5) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Pipedrive test org (18ea22be-6e0b-461c-a4db-536c022aac73) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team BigChairs Inc. (16dcfaf9-44b4-4cc2-9ead-f98dd0cad4dd) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Syncing objects for GL 500 (0c33bf2d-1c77-4200-8ed6-6147ad444c30) since 2026-02-19 13:26:04 (delay: 12s)\ndocker_lamp_1 | Team Loren X (243d7d4b-1849-46c2-9dfb-4462f52020ef) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Laravel Company 2024 (e466d657-c26d-4281-9ed5-73890fe711f6) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Barbara Fuchs (bea0fcf3-f8f9-471d-92c4-d1b7d1a2e228) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Test salesforce auto sync (2c2e652e-1cbb-4620-852d-e73bef0ab4a9) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Bullhorn (83c14120-cdb1-42e9-b0ac-16ef11320a8f) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Globo Tech (fb8d5211-3c4d-4804-9223-b091670ffd79) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Laravel 11 Company 2024 (780c6836-764b-4e47-9a05-cfefbab6e590) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Nikon7 (e9bff902-edc3-4b0b-89b1-2a9aa43d940a) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Syncing objects for Dev Zoho CRM client (1ece66c8-feb1-4df1-b321-21607daf4623) since 2026-04-22 15:14:26 (delay: 14s)\ndocker_lamp_1 | ........... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:sync-objects > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | 2026-04-22 15:44:15 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:15 Jiminny\\Jobs\\Crm\\SyncObjects .............. 49.60ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:15 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:16 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob 601.82ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:17 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:17 Jiminny\\Jobs\\Crm\\SyncObjects .............. 24.34ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:19 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:20 Jiminny\\Jobs\\Crm\\SyncObjects ................... 1s DONE\ndocker_lamp_1 | 2026-04-22 15:44:20 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:21 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob 557.11ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:21 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:21 Jiminny\\Jobs\\Crm\\SyncObjects .............. 58.10ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:23 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:24 Jiminny\\Jobs\\Crm\\SyncObjects ................... 1s DONE\ndocker_lamp_1 | 2026-04-22 15:44:25 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:26 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob 787.71ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:26 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:26 Jiminny\\Jobs\\Crm\\SyncObjects .............. 18.07ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:27 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:27 Jiminny\\Jobs\\Crm\\SyncObjects .............. 19.18ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:29 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:33 Jiminny\\Jobs\\Crm\\SyncObjects ................... 3s DONE\ndocker_lamp_1 | 2026-04-22 15:44:33 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:34 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob 848.26ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:35 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:35 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob 431.28ms DONE\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-04-22 15:45:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:03 Running ['artisan' dialers:monitor-activities] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:06 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:09 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:10 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:12 Running ['artisan' activity:purge-stale] ....... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:purge-stale > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:15 Running ['artisan' mailbox:text-relay:sync] {\ndocker_lamp_1 | \"error\": \"invalid_request\",\ndocker_lamp_1 | \"error_description\": \"Invalid impersonation \\u0026quot;sub\\u0026quot; field: @\"\ndocker_lamp_1 | }\ndocker_lamp_1 | .... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:text-relay:sync > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:17 Running ['artisan' conference:pre-meeting-notification] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:pre-meeting-notification > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:19 Running ['artisan' conference:monitor:start] ... 4s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:start > '/proc/1/fd/1' 2>&1 \nunexpected EOF\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/infrastructure/dev/docker (develop) $ work\nWARN[0000] /Users/lukas/jiminny/infrastructure/dev/docker/docker-compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion \nAttaching to blackfire-1, datadog-1, jiminny_ext-1, mariadb-1, docker_lamp_1, elasticsearch, kibana, ngrok, redis\nredis | 1:C 23 Apr 2026 07:55:51.731 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo\nredis | 1:C 23 Apr 2026 07:55:51.731 # Redis version=5.0.14, bits=64, commit=00000000, modified=0, pid=1, just started\nredis | 1:C 23 Apr 2026 07:55:51.731 # Configuration loaded\nmariadb-1 | 2026-04-23 07:55:51+00:00 [Note] [Entrypoint]: Entrypoint script for MariaDB Server 1:11.4.5+maria~ubu2404 started.\nredis | 1:M 23 Apr 2026 07:55:51.741 * Running mode=standalone, port=6379.\nredis | 1:M 23 Apr 2026 07:55:51.741 # Server initialized\nredis | 1:M 23 Apr 2026 07:55:51.741 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled.\nredis | 1:M 23 Apr 2026 07:55:51.744 * Reading RDB preamble from AOF file...\nredis | 1:M 23 Apr 2026 07:55:51.745 * Reading the remaining AOF tail...\nblackfire-1 | [2026-04-23T07:55:51Z] ERROR: The server ID parameter is not set. Please run 'blackfire-agent -register' to configure it.\nblackfire-1 | usage blackfire-agent [options]\nblackfire-1 | --collector=\"https://blackfire.io\": Sets the URL of Blackfire's data collector\nblackfire-1 | --config=\"/etc/blackfire/agent\": Sets the path to the configuration file\nblackfire-1 | -d: Prints the current configuration\nblackfire-1 | --http-proxy=\"\": Sets the HTTP proxy to use\nblackfire-1 | --https-proxy=\"\": Sets the HTTPS proxy to use\nblackfire-1 | --log-file=\"stderr\": Sets the path of the log file. Use stderr to log to stderr\nblackfire-1 | --log-level=\"1\": log verbosity level (4: debug, 3: info, 2: warning, 1: error)\nblackfire-1 | --register: Helps you with registering the agent\nblackfire-1 | --server-id=\"\": Sets the server id used to authenticate with Blackfire API\nblackfire-1 | --server-token=\"\": Sets the server token used to authenticate with Blackfire API. It is unsafe to set this from the command line\nblackfire-1 | --socket=\"unix:///var/run/blackfire/agent.sock\": Sets the socket the agent should read traces from. Possible value can be a unix socket or a TCP address. ie: unix:///var/run/blackfire/agent.sock or tcp://127.0.0.1:8307\nblackfire-1 | --test: Tests the configuration\nblackfire-1 | --timeout=\"15s\": Sets the Blackfire connection timeout\nblackfire-1 | -v: Prints the version number\nngrok | t=2026-04-23T07:55:52+0000 lvl=info msg=\"no configuration paths supplied\"\nngrok | t=2026-04-23T07:55:52+0000 lvl=info msg=\"using configuration at default config path\" path=/home/ngrok/.ngrok2/ngrok.yml\nngrok | t=2026-04-23T07:55:52+0000 lvl=info msg=\"open config file\" path=/home/ngrok/.ngrok2/ngrok.yml err=nil\nblackfire-1 exited with code 1\njiminny_ext-1 exited with code 0\nngrok | t=2026-04-23T07:55:52+0000 lvl=info msg=\"starting web service\" obj=web addr=0.0.0.0:4040\ndocker_lamp_1 | + main\ndocker_lamp_1 | + declare START_DIR\ndocker_lamp_1 | +++ realpath /scripts/init-dev\ndocker_lamp_1 | ++ dirname /scripts/init-dev\ndocker_lamp_1 | + START_DIR=/scripts\ndocker_lamp_1 | + readonly START_DIR\ndocker_lamp_1 | + source /scripts/storage_init.sh\ndocker_lamp_1 | ++ set -o errexit\ndocker_lamp_1 | ++ set -o nounset\ndocker_lamp_1 | ++ set -o pipefail\ndocker_lamp_1 | + create_bind_mount\ndocker_lamp_1 | + [[ 0 == \\1 ]]\ndocker_lamp_1 | + configure_xdebug\ndocker_lamp_1 | + j2 /root/.j2_templates/xdebug/xdebug.ini.j2\nmariadb-1 | 2026-04-23 07:55:52+00:00 [Warn] [Entrypoint]: /sys/fs/cgroup///memory.pressure not writable, functionality unavailable to MariaDB\nmariadb-1 | 2026-04-23 07:55:52+00:00 [Note] [Entrypoint]: Switching to dedicated user 'mysql'\nmariadb-1 | 2026-04-23 07:55:52+00:00 [Note] [Entrypoint]: Entrypoint script for MariaDB Server 1:11.4.5+maria~ubu2404 started.\ndatadog-1 | [s6-init] making user provided files available at /var/run/s6/etc...exited 0.\nngrok | t=2026-04-23T07:55:52+0000 lvl=info msg=\"tunnel session started\" obj=tunnels.session\nngrok | t=2026-04-23T07:55:52+0000 lvl=info msg=\"client session established\" obj=csess id=1c8f9eb905ac\nmariadb-1 | 2026-04-23 07:55:53+00:00 [Note] [Entrypoint]: MariaDB upgrade information missing, assuming required\nmariadb-1 | 2026-04-23 07:55:53+00:00 [Note] [Entrypoint]: MariaDB upgrade (mariadb-upgrade or creating healthcheck users) required, but skipped due to $MARIADB_AUTO_UPGRADE setting\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] Starting MariaDB 11.4.5-MariaDB-ubu2404 source revision 0771110266ff5c04216af4bf1243c65f8c67ccf4 server_uid zs02SyAxkg7epm72pZgde8TQHsk= as process 1\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Compressed tables use zlib 1.3\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Number of transaction pools: 1\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Using ARMv8 crc32 + pmull instructions\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Using liburing\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Initializing buffer pool, total size = 128.000MiB, chunk size = 2.000MiB\nngrok | t=2026-04-23T07:55:53+0000 lvl=info msg=\"started tunnel\" obj=tunnels name=\"command_line (http)\" addr=http://lamp:3080 url=http://lukask.ngrok.io\nngrok | t=2026-04-23T07:55:53+0000 lvl=info msg=\"started tunnel\" obj=tunnels name=command_line addr=http://lamp:3080 url=https://lukask.ngrok.io\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Completed initialization of buffer pool\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: File system buffers for log disabled (block size=512 bytes)\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: End of log at LSN=7618260638\ndocker_lamp_1 | + configure_blackfire\ndocker_lamp_1 | + j2 /root/.j2_templates/blackfire/extension.ini.j2\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Opened 3 undo tablespaces\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: 128 rollback segments in 3 undo tablespaces are active.\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Setting file './ibtmp1' size to 12.000MiB. Physically writing the file full; Please wait ...\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: File './ibtmp1' size is now 12.000MiB.\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: log sequence number 7618260638; transaction id 11490938\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] Plugin 'FEEDBACK' is disabled.\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] Plugin 'wsrep-provider' is disabled.\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Loading buffer pool(s) from /var/lib/mysql/ib_buffer_pool\ndocker_lamp_1 | + j2 /root/.j2_templates/blackfire/cli.ini.j2\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Buffer pool(s) load completed at 260423 7:55:53\nngrok | t=2026-04-23T07:55:53+0000 lvl=info msg=\"update available\" obj=updater\ndocker_lamp_1 | + declare EMPTY_DB\ndocker_lamp_1 | + db_is_empty\ndocker_lamp_1 | ++ find /var/lib/mysql/ -maxdepth 1\ndocker_lamp_1 | ++ wc -l\ndocker_lamp_1 | + [[ 11 -lt 5 ]]\ndocker_lamp_1 | + EMPTY_DB=0\ndocker_lamp_1 | + readonly EMPTY_DB\ndocker_lamp_1 | + [[ 0 -eq 1 ]]\ndocker_lamp_1 | + [[ local == \\l\\o\\c\\a\\l ]]\ndocker_lamp_1 | + set_nginx_domain dev.jiminny.com\ndocker_lamp_1 | + declare -r DOMAIN_NAME=dev.jiminny.com\ndocker_lamp_1 | + cp -f /etc/nginx/nginx_template.conf /etc/nginx/nginx.conf\ndocker_lamp_1 | + sed -i -E s~_JIMINNY_DOMAIN~app.dev.jiminny.com~g /etc/nginx/nginx.conf\ndocker_lamp_1 | + sed -i -E s~_JIMINNY_EXT_DOMAIN~ext.dev.jiminny.com~g /etc/nginx/nginx.conf\ndocker_lamp_1 | + sed -i -E s~_JIMINNY_WEB_DOMAIN~www.dev.jiminny.com~g /etc/nginx/nginx.conf\ndocker_lamp_1 | + [[ -n 3399 ]]\ndocker_lamp_1 | + sed -i -E 's~http:\\/\\/jiminny_ext:8080~http://jiminny_ext:3399~g' /etc/nginx/nginx.conf\ndocker_lamp_1 | + [[ -n host.docker.internal ]]\ndocker_lamp_1 | + sed -i -E 's~http:\\/\\/jiminny_ext~http://host.docker.internal~g' /etc/nginx/nginx.conf\ndocker_lamp_1 | + build_dev\ndocker_lamp_1 | + cd /home/jiminny/\ndocker_lamp_1 | + create_dot_env_local_file\ndocker_lamp_1 | + cp -f /home/jiminny/.env.local /home/jiminny/.env.local.bak\ndocker_lamp_1 | + create_dot_env\ndocker_lamp_1 | + [[ -f /home/jiminny/.env ]]\ndocker_lamp_1 | + return\ndocker_lamp_1 | + declare DB_ADMIN_PASSWORD\ndocker_lamp_1 | + declare DB_ADMIN_USERNAME\ndocker_lamp_1 | + declare DB_DEV_PASSWORD\ndocker_lamp_1 | + declare DB_DEV_USERNAME\ndocker_lamp_1 | + declare DB_ROOT_PASSWORD\ndocker_lamp_1 | + declare DB_ROOT_USERNAME\ndocker_lamp_1 | + declare DB_WEB_PASSWORD\ndocker_lamp_1 | + declare DB_WEB_USERNAME\ndocker_lamp_1 | ++ jq -r .DB_ADMIN_PASSWORD /home/jiminny/dev.json\ndatadog-1 | [s6-init] ensuring user provided files have correct perms...exited 0.\ndocker_lamp_1 | + DB_ADMIN_PASSWORD='dgyt$rTe21-d'\ndocker_lamp_1 | ++ jq -r .DB_ADMIN_USERNAME /home/jiminny/dev.json\ndatadog-1 | [fix-attrs.d] applying ownership & permissions fixes...\ndocker_lamp_1 | + DB_ADMIN_USERNAME=jmnyadmin\ndocker_lamp_1 | ++ jq -r .DB_DEV_PASSWORD /home/jiminny/dev.json\ndatadog-1 | [fix-attrs.d] done.\ndocker_lamp_1 | + DB_DEV_PASSWORD=rTr4sdQA65-Ad\n\n\ndatadog-1 | [cont-init.d] executing container initialization scripts...\ndocker_lamp_1 | ++ jq -r .DB_DEV_USERNAME /home/jiminny/dev.json\ndocker_lamp_1 | + DB_DEV_USERNAME=jmnydev\ndocker_lamp_1 | ++ jq -r .DB_ROOT_PASSWORD /home/jiminny/dev.json\ndatadog-1 | [cont-init.d] 01-check-apikey.sh: executing... \ndocker_lamp_1 | + DB_ROOT_PASSWORD=b7h5-1fH3e54J\ndocker_lamp_1 | ++ jq -r .DB_ROOT_USERNAME /home/jiminny/dev.json\ndocker_lamp_1 | + DB_ROOT_USERNAME=root\ndocker_lamp_1 | ++ jq -r .DB_WEB_PASSWORD /home/jiminny/dev.json\ndatadog-1 | \ndatadog-1 | ==================================================================================\ndatadog-1 | You must set an DD_API_KEY environment variable to run the Datadog Agent container\ndatadog-1 | ==================================================================================\ndatadog-1 | \ndatadog-1 | [cont-init.d] 01-check-apikey.sh: exited 1.\ndocker_lamp_1 | + DB_WEB_PASSWORD=aR5-EWf23b8da\ndocker_lamp_1 | ++ jq -r .DB_WEB_USERNAME /home/jiminny/dev.json\ndocker_lamp_1 | + DB_WEB_USERNAME=jmnyweb\ndocker_lamp_1 | + readonly DB_ADMIN_PASSWORD\ndocker_lamp_1 | + readonly DB_ADMIN_USERNAME\ndocker_lamp_1 | + readonly DB_DEV_PASSWORD\ndocker_lamp_1 | + readonly DB_DEV_USERNAME\ndocker_lamp_1 | + readonly DB_ROOT_PASSWORD\ndocker_lamp_1 | + readonly DB_ROOT_USERNAME\ndocker_lamp_1 | + readonly DB_WEB_PASSWORD\ndocker_lamp_1 | + readonly DB_WEB_USERNAME\ndocker_lamp_1 | + sed -i -E 's~DB_USERNAME=.*$~DB_USERNAME=jmnyadmin~g' /home/jiminny/.env\ndocker_lamp_1 | + sed -i -E 's~DB_PASSWORD=.*$~DB_PASSWORD=dgyt$rTe21-d~g' /home/jiminny/.env\ndocker_lamp_1 | + sed -i -E 's~DB_HOST=.*$~DB_HOST=mariadb~g' /home/jiminny/.env\ndocker_lamp_1 | + cp -f /home/jiminny/.env /home/jiminny/.env.root\ndocker_lamp_1 | + cp -f /home/jiminny/.env /home/jiminny/.env.migrate\ndocker_lamp_1 | + sed -i -E 's~DB_PASSWORD=.*$~DB_PASSWORD=b7h5-1fH3e54J~g' /home/jiminny/.env.migrate\ndocker_lamp_1 | + sed -i -E 's~DB_PASSWORD=.*$~DB_PASSWORD=b7h5-1fH3e54J~g' /home/jiminny/.env.root\ndocker_lamp_1 | + sed -i -E 's~DB_USERNAME=.*$~DB_USERNAME=root~g' /home/jiminny/.env.migrate\ndocker_lamp_1 | + sed -i -E 's~DB_USERNAME=.*$~DB_USERNAME=root~g' /home/jiminny/.env.root\ndocker_lamp_1 | + cp -f /home/jiminny/.env /home/jiminny/.env.local\ndocker_lamp_1 | + echo ''\ndocker_lamp_1 | + echo 'DB_ADMIN_PASSWORD=dgyt$rTe21-d'\ndocker_lamp_1 | + echo DB_ADMIN_USERNAME=jmnyadmin\ndocker_lamp_1 | + echo DB_DEV_PASSWORD=rTr4sdQA65-Ad\ndocker_lamp_1 | + echo DB_DEV_USERNAME=jmnydev\ndocker_lamp_1 | + echo DB_ROOT_PASSWORD=b7h5-1fH3e54J\ndocker_lamp_1 | + echo DB_ROOT_USERNAME=root\ndocker_lamp_1 | + echo DB_WEB_PASSWORD=aR5-EWf23b8da\ndocker_lamp_1 | + echo DB_WEB_USERNAME=jmnyweb\ndocker_lamp_1 | + [[ false == \\f\\a\\l\\s\\e ]]\ndocker_lamp_1 | + declare COMPOSER_PARAM=--prefer-dist\ndocker_lamp_1 | + [[ 0 -eq 1 ]]\ndocker_lamp_1 | + composer install --prefer-dist\ndatadog-1 exited with code 1\nmariadb-1 | 2026-04-23 7:55:54 0 [Note] Server socket created on IP: '0.0.0.0'.\nmariadb-1 | 2026-04-23 7:55:54 0 [Note] Server socket created on IP: '::'.\nmariadb-1 | 2026-04-23 7:55:54 0 [Note] mariadbd: Event Scheduler: Loaded 0 events\nmariadb-1 | 2026-04-23 7:55:54 0 [Note] mariadbd: ready for connections.\nmariadb-1 | Version: '11.4.5-MariaDB-ubu2404' socket: '/run/mysqld/mysqld.sock' port: 3306 mariadb.org binary distribution\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:56,514Z\", \"level\": \"INFO\", \"component\": \"o.e.n.Node\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"version[7.10.2], pid[7], build[default/docker/747e1cc71def077253878a59143c1f785afa92b9/2021-01-13T04:42:47.157277Z], OS[Linux/6.12.54-linuxkit/aarch64], JVM[AdoptOpenJDK/OpenJDK 64-Bit Server VM/15.0.1/15.0.1+9]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:56,517Z\", \"level\": \"INFO\", \"component\": \"o.e.n.Node\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"JVM home [/usr/share/elasticsearch/jdk], using bundled JDK [true]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:56,518Z\", \"level\": \"INFO\", \"component\": \"o.e.n.Node\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"JVM arguments [-Xshare:auto, -Des.networkaddress.cache.ttl=60, -Des.networkaddress.cache.negative.ttl=10, -XX:+AlwaysPreTouch, -Xss1m, -Djava.awt.headless=true, -Dfile.encoding=UTF-8, -Djna.nosys=true, -XX:-OmitStackTraceInFastThrow, -XX:+ShowCodeDetailsInExceptionMessages, -Dio.netty.noUnsafe=true, -Dio.netty.noKeySetOptimization=true, -Dio.netty.recycler.maxCapacityPerThread=0, -Dio.netty.allocator.numDirectArenas=0, -Dlog4j.shutdownHookEnabled=false, -Dlog4j2.disable.jmx=true, -Djava.locale.providers=SPI,COMPAT, -Xms1g, -Xmx1g, -XX:+UseG1GC, -XX:G1ReservePercent=25, -XX:InitiatingHeapOccupancyPercent=30, -Djava.io.tmpdir=/tmp/elasticsearch-10124985765930456581, -XX:+HeapDumpOnOutOfMemoryError, -XX:HeapDumpPath=data, -XX:ErrorFile=logs/hs_err_pid%p.log, -Xlog:gc*,gc+age=trace,safepoint:file=logs/gc.log:utctime,pid,tags:filecount=32,filesize=64m, -Des.cgroups.hierarchy.override=/, -Xms700m, -Xmx700m, -XX:MaxDirectMemorySize=367001600, -Des.path.home=/usr/share/elasticsearch, -Des.path.conf=/usr/share/elasticsearch/config, -Des.distribution.flavor=default, -Des.distribution.type=docker, -Des.bundled_jdk=true]\" }\ndocker_lamp_1 | Installing dependencies from lock file (including require-dev)\ndocker_lamp_1 | Verifying lock file contents can be installed on current platform.\ndocker_lamp_1 | Nothing to install, update or remove\ndocker_lamp_1 | Package doctrine/annotations is abandoned, you should avoid using it. No replacement was suggested.\ndocker_lamp_1 | Package symfony/debug is abandoned, you should avoid using it. Use symfony/error-handler instead.\ndocker_lamp_1 | Generating optimized autoload files\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,062Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [aggs-matrix-stats]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,062Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [analysis-common]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,063Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [constant-keyword]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,063Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [flattened]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,063Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [frozen-indices]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,063Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [ingest-common]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,063Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [ingest-geoip]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,063Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [ingest-user-agent]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,063Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [kibana]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,064Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [lang-expression]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,064Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [lang-mustache]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [lang-painless]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [mapper-extras]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [mapper-version]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [parent-join]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [percolator]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [rank-eval]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [reindex]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [repositories-metering-api]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [repository-url]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [search-business-rules]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [searchable-snapshots]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [spatial]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [transform]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [transport-netty4]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [unsigned-long]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [vectors]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [wildcard]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-analytics]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-async]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-async-search]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-autoscaling]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-ccr]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-core]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-data-streams]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-deprecation]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-enrich]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-eql]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-graph]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-identity-provider]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-ilm]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-logstash]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-ml]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-monitoring]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-ql]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-rollup]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-security]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-sql]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,068Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-stack]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,068Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-voting-only-node]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,068Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-watcher]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,068Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"no plugins loaded\" }\nelasticsearch | {\"type\": \"deprecation\", \"timestamp\": \"2026-04-23T07:55:59,122Z\", \"level\": \"DEPRECATION\", \"component\": \"o.e.d.c.s.Settings\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"[node.data] setting was deprecated in Elasticsearch and will be removed in a future release! See the breaking changes documentation for the next major version.\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,139Z\", \"level\": \"INFO\", \"component\": \"o.e.e.NodeEnvironment\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"using [1] data paths, mounts [[/usr/share/elasticsearch/data (/dev/vda1)]], net usable_space [14.5gb], net total_space [58.3gb], types [ext4]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,139Z\", \"level\": \"INFO\", \"component\": \"o.e.e.NodeEnvironment\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"heap size [700mb], compressed ordinary object pointers [true]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,263Z\", \"level\": \"INFO\", \"component\": \"o.e.n.Node\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"node name [e802ad473a4f], node ID [e2ZKzgw4Q4aCf2w5ljWr1A], cluster name [docker-cluster], roles [transform, master, remote_cluster_client, data, ml, data_content, data_hot, data_warm, data_cold, ingest]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:06,887Z\", \"level\": \"INFO\", \"component\": \"o.e.x.m.p.l.CppLogMessageHandler\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"[controller/214] [Main.cc@114] controller (64 bit): Version 7.10.2 (Build 40a3af639d4698) Copyright (c) 2020 Elasticsearch BV\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:08,335Z\", \"level\": \"INFO\", \"component\": \"o.e.t.NettyAllocator\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"creating NettyAllocator with the following configs: [name=unpooled, suggested_max_allocation_size=256kb, factors={es.unsafe.use_unpooled_allocator=null, g1gc_enabled=true, g1gc_region_size=1mb, heap_size=700mb}]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:08,444Z\", \"level\": \"INFO\", \"component\": \"o.e.d.DiscoveryModule\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"using discovery type [single-node] and seed hosts providers [settings]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:08,962Z\", \"level\": \"WARN\", \"component\": \"o.e.g.DanglingIndicesState\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"gateway.auto_import_dangling_indices is disabled, dangling indices will not be automatically detected or imported and must be managed manually\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:09,262Z\", \"level\": \"INFO\", \"component\": \"o.e.n.Node\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"initialized\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:09,263Z\", \"level\": \"INFO\", \"component\": \"o.e.n.Node\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"starting ...\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:09,430Z\", \"level\": \"INFO\", \"component\": \"o.e.t.TransportService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"publish_address {172.18.0.2:9300}, bound_addresses {[::]:9300}\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:09,769Z\", \"level\": \"INFO\", \"component\": \"o.e.c.c.Coordinator\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"cluster UUID [8uh2w1CUSGyWYR_OvaKx6g]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:09,886Z\", \"level\": \"INFO\", \"component\": \"o.e.c.s.MasterService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"elected-as-master ([1] nodes joined)[{e802ad473a4f}{e2ZKzgw4Q4aCf2w5ljWr1A}{cv7PSrNNRgOBYzeY8N23MA}{172.18.0.2}{172.18.0.2:9300}{cdhilmrstw}{ml.machine_memory=4109217792, xpack.installed=true, transform.node=true, ml.max_open_jobs=20} elect leader, _BECOME_MASTER_TASK_, _FINISH_ELECTION_], term: 229, version: 8527, delta: master node changed {previous [], current [{e802ad473a4f}{e2ZKzgw4Q4aCf2w5ljWr1A}{cv7PSrNNRgOBYzeY8N23MA}{172.18.0.2}{172.18.0.2:9300}{cdhilmrstw}{ml.machine_memory=4109217792, xpack.installed=true, transform.node=true, ml.max_open_jobs=20}]}\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:09,973Z\", \"level\": \"INFO\", \"component\": \"o.e.c.s.ClusterApplierService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"master node changed {previous [], current [{e802ad473a4f}{e2ZKzgw4Q4aCf2w5ljWr1A}{cv7PSrNNRgOBYzeY8N23MA}{172.18.0.2}{172.18.0.2:9300}{cdhilmrstw}{ml.machine_memory=4109217792, xpack.installed=true, transform.node=true, ml.max_open_jobs=20}]}, term: 229, version: 8527, reason: Publication{term=229, version=8527}\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:10,019Z\", \"level\": \"INFO\", \"component\": \"o.e.h.AbstractHttpServerTransport\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"publish_address {172.18.0.2:9200}, bound_addresses {[::]:9200}\", \"cluster.uuid\": \"8uh2w1CUSGyWYR_OvaKx6g\", \"node.id\": \"e2ZKzgw4Q4aCf2w5ljWr1A\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:10,020Z\", \"level\": \"INFO\", \"component\": \"o.e.n.Node\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"started\", \"cluster.uuid\": \"8uh2w1CUSGyWYR_OvaKx6g\", \"node.id\": \"e2ZKzgw4Q4aCf2w5ljWr1A\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:10,388Z\", \"level\": \"INFO\", \"component\": \"o.e.l.LicenseService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"license [85e882e5-5714-4173-a5dd-9baa841494a0] mode [basic] - valid\", \"cluster.uuid\": \"8uh2w1CUSGyWYR_OvaKx6g\", \"node.id\": \"e2ZKzgw4Q4aCf2w5ljWr1A\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:10,396Z\", \"level\": \"INFO\", \"component\": \"o.e.g.GatewayService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"recovered [15] indices into cluster_state\", \"cluster.uuid\": \"8uh2w1CUSGyWYR_OvaKx6g\", \"node.id\": \"e2ZKzgw4Q4aCf2w5ljWr1A\" }\nredis | 1:M 23 Apr 2026 07:56:12.207 * DB loaded from append only file: 20.465 seconds\nredis | 1:M 23 Apr 2026 07:56:12.208 * Ready to accept connections\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:12,454Z\", \"level\": \"INFO\", \"component\": \"o.e.c.r.a.AllocationService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"Cluster health status changed from [RED] to [YELLOW] (reason: [shards started [[activities][2]]]).\", \"cluster.uuid\": \"8uh2w1CUSGyWYR_OvaKx6g\", \"node.id\": \"e2ZKzgw4Q4aCf2w5ljWr1A\" }\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:26Z\",\"tags\":[\"info\",\"plugins-service\"],\"pid\":7,\"message\":\"Plugin \\\"visTypeXy\\\" is disabled.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:26Z\",\"tags\":[\"info\",\"plugins-service\"],\"pid\":7,\"message\":\"Plugin \\\"auditTrail\\\" is disabled.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:26Z\",\"tags\":[\"warning\",\"config\",\"deprecation\"],\"pid\":7,\"message\":\"Config key [monitoring.cluster_alerts.email_notifications.email_address] will be required for email notifications to work in 8.0.\\\"\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:26Z\",\"tags\":[\"info\",\"plugins-system\"],\"pid\":7,\"message\":\"Setting up [96] plugins: [taskManager,licensing,globalSearch,globalSearchProviders,code,usageCollection,xpackLegacy,telemetryCollectionManager,telemetry,telemetryCollectionXpack,kibanaUsageCollection,securityOss,newsfeed,mapsLegacy,kibanaLegacy,translations,share,legacyExport,embeddable,uiActionsEnhanced,expressions,data,home,observability,cloud,console,consoleExtensions,apmOss,searchprofiler,painlessLab,grokdebugger,management,indexPatternManagement,advancedSettings,fileUpload,savedObjects,dashboard,visualizations,visTypeTable,visTypeVega,visTypeTimelion,timelion,features,upgradeAssistant,security,snapshotRestore,enterpriseSearch,encryptedSavedObjects,ingestManager,indexManagement,remoteClusters,crossClusterReplication,indexLifecycleManagement,dashboardMode,beatsManagement,transform,ingestPipelines,maps,licenseManagement,graph,dataEnhanced,visTypeMarkdown,tileMap,regionMap,inputControlVis,visualize,esUiShared,charts,lens,visTypeVislib,visTypeTimeseries,rollup,visTypeTagcloud,visTypeMetric,watcher,discover,discoverEnhanced,savedObjectsManagement,spaces,reporting,lists,eventLog,actions,case,alerts,stackAlerts,triggersActionsUi,ml,securitySolution,infra,monitoring,logstash,apm,uptime,bfetch,canvas]\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"security\",\"config\"],\"pid\":7,\"message\":\"Generating a random key for xpack.security.encryptionKey. To prevent sessions from being invalidated on restart, please set xpack.security.encryptionKey in kibana.yml\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"security\",\"config\"],\"pid\":7,\"message\":\"Session cookies will be transmitted over insecure connections. This is not recommended.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"encryptedSavedObjects\",\"config\"],\"pid\":7,\"message\":\"Generating a random key for xpack.encryptedSavedObjects.encryptionKey. To be able to decrypt encrypted saved objects attributes after restart, please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"ingestManager\"],\"pid\":7,\"message\":\"Fleet APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"reporting\",\"config\"],\"pid\":7,\"message\":\"Generating a random key for xpack.reporting.encryptionKey. To prevent sessions from being invalidated on restart, please set xpack.reporting.encryptionKey in kibana.yml\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"reporting\",\"config\"],\"pid\":7,\"message\":\"Found 'server.host: \\\"0\\\"' in Kibana configuration. This is incompatible with Reporting. To enable Reporting to work, 'xpack.reporting.kibanaServer.hostname: 0.0.0.0' is being automatically to the configuration. You can change the setting to 'server.host: 0.0.0.0' or add 'xpack.reporting.kibanaServer.hostname: 0.0.0.0' in kibana.yml to prevent this message.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"reporting\",\"config\"],\"pid\":7,\"message\":\"Chromium sandbox provides an additional layer of protection, but is not supported for Linux CentOS 8.3.2011\\n OS. Automatically setting 'xpack.reporting.capture.browser.chromium.disableSandbox: true'.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"actions\",\"actions\"],\"pid\":7,\"message\":\"APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"alerts\",\"plugins\",\"alerting\"],\"pid\":7,\"message\":\"APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"info\",\"plugins\",\"monitoring\",\"monitoring\"],\"pid\":7,\"message\":\"config sourced from: production cluster\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"info\",\"savedobjects-service\"],\"pid\":7,\"message\":\"Waiting until all Elasticsearch nodes are compatible with Kibana before starting saved objects migrations...\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:28Z\",\"tags\":[\"info\",\"savedobjects-service\"],\"pid\":7,\"message\":\"Starting saved objects migrations\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:28Z\",\"tags\":[\"info\",\"plugins-system\"],\"pid\":7,\"message\":\"Starting [96] plugins: [taskManager,licensing,globalSearch,globalSearchProviders,code,usageCollection,xpackLegacy,telemetryCollectionManager,telemetry,telemetryCollectionXpack,kibanaUsageCollection,securityOss,newsfeed,mapsLegacy,kibanaLegacy,translations,share,legacyExport,embeddable,uiActionsEnhanced,expressions,data,home,observability,cloud,console,consoleExtensions,apmOss,searchprofiler,painlessLab,grokdebugger,management,indexPatternManagement,advancedSettings,fileUpload,savedObjects,dashboard,visualizations,visTypeTable,visTypeVega,visTypeTimelion,timelion,features,upgradeAssistant,security,snapshotRestore,enterpriseSearch,encryptedSavedObjects,ingestManager,indexManagement,remoteClusters,crossClusterReplication,indexLifecycleManagement,dashboardMode,beatsManagement,transform,ingestPipelines,maps,licenseManagement,graph,dataEnhanced,visTypeMarkdown,tileMap,regionMap,inputControlVis,visualize,esUiShared,charts,lens,visTypeVislib,visTypeTimeseries,rollup,visTypeTagcloud,visTypeMetric,watcher,discover,discoverEnhanced,savedObjectsManagement,spaces,reporting,lists,eventLog,actions,case,alerts,stackAlerts,triggersActionsUi,ml,securitySolution,infra,monitoring,logstash,apm,uptime,bfetch,canvas]\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:28Z\",\"tags\":[\"info\",\"plugins\",\"taskManager\",\"taskManager\"],\"pid\":7,\"message\":\"TaskManager is identified by the Kibana UUID: bf01f365-e094-4cde-940d-3e0db65fa22a\"}\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:28,778Z\", \"level\": \"INFO\", \"component\": \"o.e.c.m.MetadataIndexTemplateService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"adding template [.management-beats] for index patterns [.management-beats]\", \"cluster.uuid\": \"8uh2w1CUSGyWYR_OvaKx6g\", \"node.id\": \"e2ZKzgw4Q4aCf2w5ljWr1A\" }\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:28Z\",\"tags\":[\"info\",\"plugins\",\"crossClusterReplication\"],\"pid\":7,\"message\":\"Your basic license does not support crossClusterReplication. Please upgrade your license.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:28Z\",\"tags\":[\"info\",\"plugins\",\"watcher\"],\"pid\":7,\"message\":\"Your basic license does not support watcher. Please upgrade your license.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:28Z\",\"tags\":[\"info\",\"plugins\",\"monitoring\",\"monitoring\",\"kibana-monitoring\"],\"pid\":7,\"message\":\"Starting monitoring stats collection\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:29Z\",\"tags\":[\"error\",\"elasticsearch\",\"data\"],\"pid\":7,\"message\":\"[version_conflict_engine_exception]: [task:Lens-lens_telemetry]: version conflict, document already exists (current version [745])\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:29Z\",\"tags\":[\"error\",\"elasticsearch\",\"data\"],\"pid\":7,\"message\":\"[version_conflict_engine_exception]: [task:Actions-actions_telemetry]: version conflict, document already exists (current version [745])\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:29Z\",\"tags\":[\"error\",\"elasticsearch\",\"data\"],\"pid\":7,\"message\":\"[version_conflict_engine_exception]: [task:endpoint:user-artifact-packager:1.0.0]: version conflict, document already exists (current version [310913])\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:29Z\",\"tags\":[\"error\",\"elasticsearch\",\"data\"],\"pid\":7,\"message\":\"[version_conflict_engine_exception]: [task:Alerting-alerting_telemetry]: version conflict, document already exists (current version [745])\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:29Z\",\"tags\":[\"error\",\"elasticsearch\",\"data\"],\"pid\":7,\"message\":\"[version_conflict_engine_exception]: [task:apm-telemetry-task]: version conflict, document already exists (current version [1222])\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:29Z\",\"tags\":[\"listening\",\"info\"],\"pid\":7,\"message\":\"Server running at http://0:5601\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:31Z\",\"tags\":[\"info\",\"http\",\"server\",\"Kibana\"],\"pid\":7,\"message\":\"http server running at http://0:5601\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:32Z\",\"tags\":[\"warning\",\"plugins\",\"reporting\"],\"pid\":7,\"message\":\"Enabling the Chromium sandbox provides an additional layer of protection.\"}\n\n\nv View in Docker Desktop o View Config w Enable Watch","is_focused":true},{"role":"AXButton","text":"Menu","depth":3,"bounds":{"left":0.50232714,"top":1.0,"width":0.004986702,"height":-0.06424582},"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"⌥1 DOCKER (docker-compose)","depth":3,"bounds":{"left":0.27925533,"top":1.0,"width":0.22140957,"height":-0.06464481},"role_description":"text"},{"role":"AXTextArea","text":"Last login: Mon Apr 20 19:47:56 on console\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ prod\n(lukas@jiminny-prod-bastion) Verification code: \nWelcome to Ubuntu 22.04.5 LTS (GNU/Linux 6.8.0-1041-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Tue Apr 21 06:17:09 UTC 2026\n\n System load: 0.0 Processes: 126\n Usage of /: 58.6% of 7.57GB Users logged in: 2\n Memory usage: 36% IPv4 address for eth0: 10.30.45.167\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n37 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\n*** System restart required ***\nLast login: Mon Apr 20 15:14:15 2026 from 212.5.153.87\nlukas@jiminny-prod-bastion:~$ client_loop: send disconnect: Broken pipe\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ prod\n(lukas@jiminny-prod-bastion) Verification code: \nWelcome to Ubuntu 22.04.5 LTS (GNU/Linux 6.8.0-1041-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Wed Apr 22 08:09:37 UTC 2026\n\n System load: 0.0 Processes: 158\n Usage of /: 58.7% of 7.57GB Users logged in: 7\n Memory usage: 41% IPv4 address for eth0: 10.30.45.167\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n37 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\n*** System restart required ***\nLast login: Tue Apr 21 06:17:09 2026 from 212.5.153.87\nlukas@jiminny-prod-bastion:~$ client_loop: send disconnect: Broken pipe\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ prod\n(lukas@jiminny-prod-bastion) Verification code: \nWelcome to Ubuntu 22.04.5 LTS (GNU/Linux 6.8.0-1041-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Wed Apr 22 12:09:08 UTC 2026\n\n System load: 0.0 Processes: 168\n Usage of /: 58.8% of 7.57GB Users logged in: 7\n Memory usage: 43% IPv4 address for eth0: 10.30.45.167\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n37 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\n*** System restart required ***\nLast login: Wed Apr 22 08:09:38 2026 from 212.5.153.87\nlukas@jiminny-prod-bastion:~$ client_loop: send disconnect: Broken pipe\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","depth":5,"value":"Last login: Mon Apr 20 19:47:56 on console\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ prod\n(lukas@jiminny-prod-bastion) Verification code: \nWelcome to Ubuntu 22.04.5 LTS (GNU/Linux 6.8.0-1041-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Tue Apr 21 06:17:09 UTC 2026\n\n System load: 0.0 Processes: 126\n Usage of /: 58.6% of 7.57GB Users logged in: 2\n Memory usage: 36% IPv4 address for eth0: 10.30.45.167\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n37 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\n*** System restart required ***\nLast login: Mon Apr 20 15:14:15 2026 from 212.5.153.87\nlukas@jiminny-prod-bastion:~$ client_loop: send disconnect: Broken pipe\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ prod\n(lukas@jiminny-prod-bastion) Verification code: \nWelcome to Ubuntu 22.04.5 LTS (GNU/Linux 6.8.0-1041-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Wed Apr 22 08:09:37 UTC 2026\n\n System load: 0.0 Processes: 158\n Usage of /: 58.7% of 7.57GB Users logged in: 7\n Memory usage: 41% IPv4 address for eth0: 10.30.45.167\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n37 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\n*** System restart required ***\nLast login: Tue Apr 21 06:17:09 2026 from 212.5.153.87\nlukas@jiminny-prod-bastion:~$ client_loop: send disconnect: Broken pipe\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ prod\n(lukas@jiminny-prod-bastion) Verification code: \nWelcome to Ubuntu 22.04.5 LTS (GNU/Linux 6.8.0-1041-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Wed Apr 22 12:09:08 UTC 2026\n\n System load: 0.0 Processes: 168\n Usage of /: 58.8% of 7.57GB Users logged in: 7\n Memory usage: 43% IPv4 address for eth0: 10.30.45.167\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n37 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\n*** System restart required ***\nLast login: Wed Apr 22 08:09:38 2026 from 212.5.153.87\nlukas@jiminny-prod-bastion:~$ client_loop: send disconnect: Broken pipe\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","is_focused":true},{"role":"AXButton","text":"Menu","depth":4,"bounds":{"left":0.74202126,"top":1.0,"width":0.004986702,"height":-0.06424582},"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"⌥2 PROD (-zsh)","depth":4,"bounds":{"left":0.51861703,"top":1.0,"width":0.22174202,"height":-0.06464481},"role_description":"text"},{"role":"AXTextArea","text":"Last login: Mon Apr 20 19:48:03 on ttys001\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ eu\n(lukas@jiminny-eu-bastion) Verification code: \nWelcome to Ubuntu 22.04.4 LTS (GNU/Linux 6.8.0-1047-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Tue Apr 21 06:17:15 UTC 2026\n\n System load: 0.0 Processes: 119\n Usage of /: 58.0% of 7.57GB Users logged in: 2\n Memory usage: 19% IPv4 address for eth0: 10.20.163.228\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n85 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\n*** System restart required ***\nLast login: Mon Apr 20 15:14:23 2026 from 212.5.153.87\nlukas@jiminny-eu-bastion:~$ client_loop: send disconnect: Broken pipe\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ eu\n(lukas@jiminny-eu-bastion) Verification code: \nWelcome to Ubuntu 22.04.4 LTS (GNU/Linux 6.8.0-1047-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Tue Apr 21 16:24:08 UTC 2026\n\n System load: 0.0 Processes: 139\n Usage of /: 58.0% of 7.57GB Users logged in: 3\n Memory usage: 21% IPv4 address for eth0: 10.20.163.228\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n85 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\n*** System restart required ***\nLast login: Tue Apr 21 06:17:16 2026 from 212.5.153.87\nlukas@jiminny-eu-bastion:~$ client_loop: send disconnect: Broken pipe\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ eu\n(lukas@jiminny-eu-bastion) Verification code: \nWelcome to Ubuntu 22.04.4 LTS (GNU/Linux 6.8.0-1047-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Wed Apr 22 08:09:45 UTC 2026\n\n System load: 0.0 Processes: 133\n Usage of /: 58.7% of 7.57GB Users logged in: 4\n Memory usage: 21% IPv4 address for eth0: 10.20.163.228\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n85 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\n*** System restart required ***\nLast login: Tue Apr 21 16:24:08 2026 from 212.5.153.87\nlukas@jiminny-eu-bastion:~$","depth":5,"value":"Last login: Mon Apr 20 19:48:03 on ttys001\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ eu\n(lukas@jiminny-eu-bastion) Verification code: \nWelcome to Ubuntu 22.04.4 LTS (GNU/Linux 6.8.0-1047-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Tue Apr 21 06:17:15 UTC 2026\n\n System load: 0.0 Processes: 119\n Usage of /: 58.0% of 7.57GB Users logged in: 2\n Memory usage: 19% IPv4 address for eth0: 10.20.163.228\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n85 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\n*** System restart required ***\nLast login: Mon Apr 20 15:14:23 2026 from 212.5.153.87\nlukas@jiminny-eu-bastion:~$ client_loop: send disconnect: Broken pipe\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ eu\n(lukas@jiminny-eu-bastion) Verification code: \nWelcome to Ubuntu 22.04.4 LTS (GNU/Linux 6.8.0-1047-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Tue Apr 21 16:24:08 UTC 2026\n\n System load: 0.0 Processes: 139\n Usage of /: 58.0% of 7.57GB Users logged in: 3\n Memory usage: 21% IPv4 address for eth0: 10.20.163.228\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n85 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\n*** System restart required ***\nLast login: Tue Apr 21 06:17:16 2026 from 212.5.153.87\nlukas@jiminny-eu-bastion:~$ client_loop: send disconnect: Broken pipe\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ eu\n(lukas@jiminny-eu-bastion) Verification code: \nWelcome to Ubuntu 22.04.4 LTS (GNU/Linux 6.8.0-1047-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Wed Apr 22 08:09:45 UTC 2026\n\n System load: 0.0 Processes: 133\n Usage of /: 58.7% of 7.57GB Users logged in: 4\n Memory usage: 21% IPv4 address for eth0: 10.20.163.228\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n85 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\n*** System restart required ***\nLast login: Tue Apr 21 16:24:08 2026 from 212.5.153.87\nlukas@jiminny-eu-bastion:~$","is_focused":true},{"role":"AXButton","text":"Menu","depth":4,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"⌥3 EU (ssh)","depth":4,"role_description":"text"},{"role":"AXTextArea","text":"Last login: Mon Apr 20 19:48:04 on ttys002\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ stg\n(lukas@jiminny-stage-bastion) Verification code: \nWelcome to Ubuntu 22.04.4 LTS (GNU/Linux 6.8.0-1052-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Tue Apr 21 14:39:05 UTC 2026\n\n System load: 0.0 Processes: 137\n Usage of /: 57.6% of 7.57GB Users logged in: 4\n Memory usage: 30% IPv4 address for ens5: 10.30.46.154\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n85 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\nLast login: Thu Apr 16 07:34:39 2026 from 212.39.71.189\nlukas@jiminny-stage-bastion:~$ client_loop: send disconnect: Broken pipe\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","depth":5,"bounds":{"left":0.50964093,"top":0.82202715,"width":0.2393617,"height":0.17797285},"value":"Last login: Mon Apr 20 19:48:04 on ttys002\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ stg\n(lukas@jiminny-stage-bastion) Verification code: \nWelcome to Ubuntu 22.04.4 LTS (GNU/Linux 6.8.0-1052-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Tue Apr 21 14:39:05 UTC 2026\n\n System load: 0.0 Processes: 137\n Usage of /: 57.6% of 7.57GB Users logged in: 4\n Memory usage: 30% IPv4 address for ens5: 10.30.46.154\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n85 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\nLast login: Thu Apr 16 07:34:39 2026 from 212.39.71.189\nlukas@jiminny-stage-bastion:~$ client_loop: send disconnect: Broken pipe\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","is_focused":true},{"role":"AXButton","text":"Menu","depth":4,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"⌥4 STAGE (-zsh)","depth":4,"role_description":"text"},{"role":"AXTextArea","text":"Last login: Mon Apr 20 19:48:04 on ttys002\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","depth":5,"value":"Last login: Mon Apr 20 19:48:04 on ttys002\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","is_focused":true},{"role":"AXButton","text":"Menu","depth":4,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"⌥5 QA (-zsh)","depth":4,"role_description":"text"},{"role":"AXTextArea","text":"Last login: Mon Apr 20 19:48:04 on ttys004\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","depth":5,"value":"Last login: Mon Apr 20 19:48:04 on ttys004\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","is_focused":true},{"role":"AXButton","text":"Menu","depth":4,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"⌥6 FE (-zsh)","depth":4,"role_description":"text"},{"role":"AXTextArea","text":"Last login: Mon Apr 20 19:48:04 on ttys005\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","depth":5,"value":"Last login: Mon Apr 20 19:48:04 on ttys005\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","is_focused":true},{"role":"AXButton","text":"Menu","depth":4,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"⌥7 EXT (-zsh)","depth":4,"role_description":"text"},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.32912233,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.33111703,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.3879654,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.3899601,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"✳ Build full day activity summary from Screenpipe (claude)","depth":2,"bounds":{"left":0.44680852,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4488032,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.5056516,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.50764626,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.56449467,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.56648934,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.62333775,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6253325,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ec2-user@ip-10-30-159-186:~ (-zsh)","depth":2,"bounds":{"left":0.6821808,"top":1.0,"width":0.058843084,"height":-0.042298436},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.68417555,"top":1.0,"width":0.005319149,"height":-0.04549086},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7273936,"top":1.0,"width":0.01861702,"height":-0.023144484},"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"DOCKER (docker-compose)","depth":1,"bounds":{"left":0.47905585,"top":1.0,"width":0.060837764,"height":-0.02394259},"role_description":"text"}]...
|
-3009288458992913716
|
-8305438663760624882
|
idle
|
accessibility
|
NULL
|
0 social account(s) to be processed ...
docker_lam 0 social account(s) to be processed ...
docker_lamp_1 |
docker_lamp_1 | Done!
docker_lamp_1 | 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:bullhorn:ping --heartbeat > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 |
docker_lamp_1 | 2026-04-22 15:41:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:41:04 Running ['artisan' dialers:monitor-activities] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:41:06 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:41:07 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:41:08 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:41:10 Running ['artisan' crm:sync-hubspot-objects] [HubSpot] Syncing objects for Hubspot (abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4) since 2026-04-22 15:36:20 (delay: 0s)
docker_lamp_1 | Team TestV (2708d27a-7f31-4d90-bd2b-b5bde1c0211e) is not yet assigned an owner. skipping...
docker_lamp_1 | Team KioskAccount (dedab245-604c-4cd2-a83e-4b9e034b2772) is not yet assigned an owner. skipping...
docker_lamp_1 | [HubSpot] Syncing objects for InsightSquared (b2b115eb-93ce-4d1b-929c-173757df8fba) since 2026-02-17 15:09:59 (delay: 0s)
docker_lamp_1 | Team Aircall Demo (7980e5eb-b11c-4cee-9c21-8bb29ba85f3b) is not yet assigned an owner. skipping...
docker_lamp_1 | [HubSpot] Syncing objects for GoStudent UAT (b2d49a54-b645-4637-a7ae-a86cfce6e8e4) since 2026-02-17 15:09:59 (delay: 1s)
docker_lamp_1 | [HubSpot] Syncing objects for JustCall (c6b9d6b0-b48d-4832-a68c-a57d60651888) since 2026-02-17 15:07:41 (delay: 1s)
docker_lamp_1 | Team Twilio Video (c334ca55-b230-411c-b10e-31c8204bd07b) is not yet assigned an owner. skipping...
docker_lamp_1 | Team My Test Account 3000 (dbc9990d-b35f-4e38-9550-22cdd6059514) is not yet assigned an owner. skipping...
docker_lamp_1 | Team test (7997eb70-8aa4-491a-870d-311977568df4) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Test (5e06dcee-0613-470e-9a77-2c283198f3bf) is not yet assigned an owner. skipping...
docker_lamp_1 | Team test ogg auto sync (da44776e-306f-427a-83d8-a1b4baa5537e) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Tourlaner (d9b71080-388b-4cf5-8175-aa0f29bee635) is not yet assigned an owner. skipping...
docker_lamp_1 | Dispatched 4 HubSpot sync jobs
docker_lamp_1 | 2026-04-22 15:41:11 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:sync-hubspot-objects > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | 2026-04-22 15:41:11 Jiminny\Jobs\Crm\SyncHubspotObjects ....... 44.08ms DONE
docker_lamp_1 | 2026-04-22 15:41:11 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 | 2026-04-22 15:41:12 Jiminny\Jobs\Crm\SyncHubspotObjects ...... 577.01ms DONE
docker_lamp_1 | 2026-04-22 15:41:12 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:41:12 Jiminny\Jobs\Crm\SyncHubspotObjects ....... 16.42ms DONE
docker_lamp_1 | 2026-04-22 15:41:12 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:41:12 Jiminny\Jobs\Crm\SyncHubspotObjects ....... 17.23ms DONE
docker_lamp_1 |
docker_lamp_1 | 2026-04-22 15:42:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:42:03 Running ['artisan' dialers:monitor-activities] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:42:05 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:42:06 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:42:07 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:42:09 Running ['artisan' conference:monitor:count] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:42:10 Running ['artisan' mailbox:batch:create] ....... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:create > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 | 2026-04-22 15:42:14 Jiminny\Jobs\Mailbox\CreateBatches [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:42:14 Jiminny\Jobs\Mailbox\CreateBatches [PASSWORD_DOTS] 87.60ms DONE
docker_lamp_1 |
docker_lamp_1 | 2026-04-22 15:43:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:43:04 Running ['artisan' dialers:monitor-activities] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:43:06 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:43:08 Running ['artisan' mailbox:skip-lists:refresh] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:43:10 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:43:11 Running ['artisan' mailbox:batch:retry-failed --max-batches=15] in background 5.15ms DONE
docker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' mailbox:batch:retry-failed --max-batches=15 > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish "framework/schedule-390defd641effba0f73a895e426ded4cf2ba7f11" "$?") > '/dev/null' 2>&1 &
docker_lamp_1 | 2026-04-22 15:43:11 Running ['artisan' calendar:sync --dateMode=daily] 2026-04-22 15:43:19 Jiminny\Jobs\Calendar\SyncCalendarEvents ....... RUNNING
docker_lamp_1 | 2026-04-22 15:43:20 Jiminny\Jobs\Calendar\SyncCalendarEvents ....... 1s DONE
docker_lamp_1 | 2026-04-22 15:43:22 Jiminny\Jobs\Calendar\SyncCalendarEvents ....... RUNNING
docker_lamp_1 | 10s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' calendar:sync --dateMode=daily > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 | 2026-04-22 15:43:23 Jiminny\Jobs\Calendar\SyncCalendarEvents . 596.48ms DONE
docker_lamp_1 | 2026-04-22 15:43:23 Jiminny\Jobs\Calendar\SyncCalendarEvents ....... RUNNING
docker_lamp_1 | 2026-04-22 15:43:23 Jiminny\Jobs\Calendar\SyncCalendarEvents .. 52.11ms DONE
docker_lamp_1 |
docker_lamp_1 | 2026-04-22 15:44:02 Running ['artisan' meeting-bot:schedule-bot] ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:44:04 Running ['artisan' dialers:monitor-activities] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:44:05 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:44:07 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:44:09 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:44:11 Running ['artisan' conference:monitor:count] ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:44:13 Running ['artisan' crm:sync-objects] Syncing objects for Salesforce Team (6473c918-d8db-4ded-a52b-4febfd7b7c02) since 2026-03-19 20:14:10 (delay: 0s)
docker_lamp_1 | Team Edge Communications (c3a725ef-fc5e-4c7b-9231-6379b2834101) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Burlington Textiles Corp of America (72c0ccae-28a5-44bd-a0b4-958e73135463) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Uber (1c523aed-80e3-4c44-b215-7f0b027a03a2) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Vonage (c6030540-121e-4d6b-9261-8d82105817bf) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Nexmo (eaba09e1-4973-4077-b041-ad389248318c) is not yet assigned an owner. skipping...
docker_lamp_1 | Team SF (018e2d7c-ed01-4af2-be33-5c28f454a185) is not yet assigned an owner. skipping...
docker_lamp_1 | Team NewAccount (27325244-25d7-4431-aac4-9ddb2bde6b45) is not yet assigned an owner. skipping...
docker_lamp_1 | Syncing objects for Pipedrive, Inc. (51467630-d89d-480b-be20-933e64a042f7) since 2026-04-08 18:14:13 (delay: 2s)
docker_lamp_1 | Syncing objects for Copper (396ed57c-e3c4-49be-8290-37c32955f7c7) since 2026-04-22 15:14:16 (delay: 4s)
docker_lamp_1 | Syncing objects for Pipedrive External Test (fda3cbdf-1117-4ba5-86f8-775f548b3a28) since 2026-04-08 18:14:16 (delay: 6s)
docker_lamp_1 | Syncing objects for Close (3ff5a02a-86fb-4357-b1d6-a04e26c38602) since 2026-04-22 15:14:20 (delay: 8s)
docker_lamp_1 | Team DeewanyAccount (cfaf90cb-ecf0-432c-a655-a22d9985e4c8) is not yet assigned an owner. skipping...
docker_lamp_1 | Syncing objects for Bullhorn (1640a0ac-19da-4c3b-90f7-87525f07a6d2) since 2026-02-19 13:28:05 (delay: 10s)
docker_lamp_1 | Team Horen test (93d8f8f2-dd61-4137-97de-a02a9d3751bb) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Ahmed Hamadi (191379e4-5e90-4419-8ffd-4804a5abb565) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Ahmed Testing (3b2e1fad-bc5f-4322-84d2-a7440f78c5d7) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Ahmed jiminny (1dbd47b8-bbf8-4bbb-8e28-67b9c91f566e) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Testers Inc (ec0767d5-1248-4447-b316-94dd9a30d52b) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Copper Team (817c8ce1-8e22-4ac8-8fca-8df1131963ab) is not yet assigned an owner. skipping...
docker_lamp_1 | Team TheBestPlace Ever (dbb998f9-70eb-4f11-a9f2-d9f6157f4035) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Horen's apartments (e10bb4ea-6c89-4fe3-be3b-0a278aa81180) is not yet assigned an owner. skipping...
docker_lamp_1 | Team New Org Test (2cb527b6-73a4-46be-b7b2-b0097b494cc5) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Pipedrive test org (18ea22be-6e0b-461c-a4db-536c022aac73) is not yet assigned an owner. skipping...
docker_lamp_1 | Team BigChairs Inc. (16dcfaf9-44b4-4cc2-9ead-f98dd0cad4dd) is not yet assigned an owner. skipping...
docker_lamp_1 | Syncing objects for GL 500 (0c33bf2d-1c77-4200-8ed6-6147ad444c30) since 2026-02-19 13:26:04 (delay: 12s)
docker_lamp_1 | Team Loren X (243d7d4b-1849-46c2-9dfb-4462f52020ef) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Laravel Company 2024 (e466d657-c26d-4281-9ed5-73890fe711f6) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Barbara Fuchs (bea0fcf3-f8f9-471d-92c4-d1b7d1a2e228) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Test salesforce auto sync (2c2e652e-1cbb-4620-852d-e73bef0ab4a9) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Bullhorn (83c14120-cdb1-42e9-b0ac-16ef11320a8f) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Globo Tech (fb8d5211-3c4d-4804-9223-b091670ffd79) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Laravel 11 Company 2024 (780c6836-764b-4e47-9a05-cfefbab6e590) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Nikon7 (e9bff902-edc3-4b0b-89b1-2a9aa43d940a) is not yet assigned an owner. skipping...
docker_lamp_1 | Syncing objects for Dev Zoho CRM client (1ece66c8-feb1-4df1-b321-21607daf4623) since 2026-04-22 15:14:26 (delay: 14s)
docker_lamp_1 | [PASSWORD_DOTS] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:sync-objects > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 | 2026-04-22 15:44:15 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:15 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 49.60ms DONE
docker_lamp_1 | 2026-04-22 15:44:15 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob RUNNING
docker_lamp_1 | 2026-04-22 15:44:16 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob 601.82ms DONE
docker_lamp_1 | 2026-04-22 15:44:17 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:17 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 24.34ms DONE
docker_lamp_1 | 2026-04-22 15:44:19 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:20 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 1s DONE
docker_lamp_1 | 2026-04-22 15:44:20 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob RUNNING
docker_lamp_1 | 2026-04-22 15:44:21 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob 557.11ms DONE
docker_lamp_1 | 2026-04-22 15:44:21 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:21 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 58.10ms DONE
docker_lamp_1 | 2026-04-22 15:44:23 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:24 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 1s DONE
docker_lamp_1 | 2026-04-22 15:44:25 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob RUNNING
docker_lamp_1 | 2026-04-22 15:44:26 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob 787.71ms DONE
docker_lamp_1 | 2026-04-22 15:44:26 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:26 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 18.07ms DONE
docker_lamp_1 | 2026-04-22 15:44:27 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:27 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 19.18ms DONE
docker_lamp_1 | 2026-04-22 15:44:29 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:33 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 3s DONE
docker_lamp_1 | 2026-04-22 15:44:33 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob RUNNING
docker_lamp_1 | 2026-04-22 15:44:34 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob 848.26ms DONE
docker_lamp_1 | 2026-04-22 15:44:35 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob RUNNING
docker_lamp_1 | 2026-04-22 15:44:35 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob 431.28ms DONE
docker_lamp_1 |
docker_lamp_1 | 2026-04-22 15:45:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:03 Running ['artisan' dialers:monitor-activities] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:06 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:09 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:10 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:12 Running ['artisan' activity:purge-stale] ....... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:purge-stale > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:15 Running ['artisan' mailbox:text-relay:sync] {
docker_lamp_1 | "error": "invalid_request",
docker_lamp_1 | "error_description": "Invalid impersonation \u0026quot;sub\u0026quot; field: @"
docker_lamp_1 | }
docker_lamp_1 | .... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:text-relay:sync > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:17 Running ['artisan' conference:pre-meeting-notification] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:pre-meeting-notification > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:19 Running ['artisan' conference:monitor:start] ... 4s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:start > '/proc/1/fd/1' 2>&1
unexpected EOF
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/infrastructure/dev/docker (develop) $ work
WARN[0000] /Users/lukas/jiminny/infrastructure/dev/docker/docker-compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion
Attaching to blackfire-1, datadog-1, jiminny_ext-1, mariadb-1, docker_lamp_1, elasticsearch, kibana, ngrok, redis
redis | 1:C 23 Apr 2026 07:55:51.731 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
redis | 1:C 23 Apr 2026 07:55:51.731 # Redis version=5.0.14, bits=64, commit=00000000, modified=0, pid=1, just started
redis | 1:C 23 Apr 2026 07:55:51.731 # Configuration loaded
mariadb-1 | 2026-04-23 07:55:51+00:00 [Note] [Entrypoint]: Entrypoint script for MariaDB Server 1:11.4.5+maria~ubu2404 started.
redis | 1:M 23 Apr 2026 07:55:51.741 * Running mode=standalone, port=6379.
redis | 1:M 23 Apr 2026 07:55:51.741 # Server initialized
redis | 1:M 23 Apr 2026 07:55:51.741 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled.
redis | 1:M 23 Apr 2026 07:55:51.744 * Reading RDB preamble from AOF file...
redis | 1:M 23 Apr 2026 07:55:51.745 * Reading the remaining AOF tail...
blackfire-1 | [2026-04-23T07:55:51Z] ERROR: The server ID parameter is not set. Please run 'blackfire-agent -register' to configure it.
blackfire-1 | usage blackfire-agent [options]
blackfire-1 | --collector="[URL_WITH_CREDENTIALS] controller (64 bit): Version 7.10.2 (Build 40a3af639d4698) Copyright (c) 2020 Elasticsearch BV" }
elasticsearch | {"type": "server", "timestamp": "2026-04-23T07:56:08,335Z", "level": "INFO", "component": "o.e.t.NettyAllocator", "cluster.name": "docker-cluster", "node.name": "e802ad473a4f", "message": "creating NettyAllocator with the following configs: [name=unpooled, suggested_max_allocation_size=256kb, factors={es.unsafe.use_unpooled_allocator=null, g1gc_enabled=true, g1gc_region_size=1mb, heap_size=700mb}]" }
elasticsearch | {"type": "server", "timestamp": "2026-04-23T07:56:08,444Z", "level": "INFO", "component": "o.e.d.DiscoveryModule", "cluster.name": "docker-cluster", "node.name": "e802ad473a4f", "message": "using discovery type [single-node] and seed hosts providers [settings]" }
elasticsearch | {"type": "server", "timestamp": "2026-04-23T07:56:08,962Z", "level": "WARN", "component": "o.e.g.DanglingIndicesState", "cluster.name": "docker-cluster", "node.name": "e802ad473a4f", "message": "gateway.auto_import_dangling_indices is disabled, dangling indices will not be automatically detected or imported and must be managed manually" }
elasticsearch | {"type": "server", "timestamp": "2026-04-23T07:56:09,262Z", "level": "INFO", "component": "o.e.n.Node", "cluster.name": "docker-cluster", "node.name": "e802ad473a4f", "message": "initialized" }
elasticsearch | {"type": "server", "timestamp": "2026-04-23T07:56:09,263Z", "level": "INFO", "component": "o.e.n.Node", "cluster.name": "docker-cluster", "node.name": "e802ad473a4f", "message": "starting ..." }
elasticsearch | {"type": "server", "timestamp": "2026-04-23T07:56:09,430Z", "level": "INFO", "component": "o.e.t.TransportService", "cluster.name": "docker-cluster", "node.name...
|
73565
|
|
73567
|
NULL
|
0
|
2026-04-23T07:56:35.420374+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776930995420_m1.jpg...
|
iTerm2
|
DOCKER (docker-compose)
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
0 social account(s) to be processed ...
docker_lam 0 social account(s) to be processed ...
docker_lamp_1 |
docker_lamp_1 | Done!
docker_lamp_1 | 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:bullhorn:ping --heartbeat > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 |
docker_lamp_1 | 2026-04-22 15:41:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:41:04 Running ['artisan' dialers:monitor-activities] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:41:06 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:41:07 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:41:08 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:41:10 Running ['artisan' crm:sync-hubspot-objects] [HubSpot] Syncing objects for Hubspot (abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4) since 2026-04-22 15:36:20 (delay: 0s)
docker_lamp_1 | Team TestV (2708d27a-7f31-4d90-bd2b-b5bde1c0211e) is not yet assigned an owner. skipping...
docker_lamp_1 | Team KioskAccount (dedab245-604c-4cd2-a83e-4b9e034b2772) is not yet assigned an owner. skipping...
docker_lamp_1 | [HubSpot] Syncing objects for InsightSquared (b2b115eb-93ce-4d1b-929c-173757df8fba) since 2026-02-17 15:09:59 (delay: 0s)
docker_lamp_1 | Team Aircall Demo (7980e5eb-b11c-4cee-9c21-8bb29ba85f3b) is not yet assigned an owner. skipping...
docker_lamp_1 | [HubSpot] Syncing objects for GoStudent UAT (b2d49a54-b645-4637-a7ae-a86cfce6e8e4) since 2026-02-17 15:09:59 (delay: 1s)
docker_lamp_1 | [HubSpot] Syncing objects for JustCall (c6b9d6b0-b48d-4832-a68c-a57d60651888) since 2026-02-17 15:07:41 (delay: 1s)
docker_lamp_1 | Team Twilio Video (c334ca55-b230-411c-b10e-31c8204bd07b) is not yet assigned an owner. skipping...
docker_lamp_1 | Team My Test Account 3000 (dbc9990d-b35f-4e38-9550-22cdd6059514) is not yet assigned an owner. skipping...
docker_lamp_1 | Team test (7997eb70-8aa4-491a-870d-311977568df4) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Test (5e06dcee-0613-470e-9a77-2c283198f3bf) is not yet assigned an owner. skipping...
docker_lamp_1 | Team test ogg auto sync (da44776e-306f-427a-83d8-a1b4baa5537e) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Tourlaner (d9b71080-388b-4cf5-8175-aa0f29bee635) is not yet assigned an owner. skipping...
docker_lamp_1 | Dispatched 4 HubSpot sync jobs
docker_lamp_1 | 2026-04-22 15:41:11 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:sync-hubspot-objects > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | 2026-04-22 15:41:11 Jiminny\Jobs\Crm\SyncHubspotObjects ....... 44.08ms DONE
docker_lamp_1 | 2026-04-22 15:41:11 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 | 2026-04-22 15:41:12 Jiminny\Jobs\Crm\SyncHubspotObjects ...... 577.01ms DONE
docker_lamp_1 | 2026-04-22 15:41:12 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:41:12 Jiminny\Jobs\Crm\SyncHubspotObjects ....... 16.42ms DONE
docker_lamp_1 | 2026-04-22 15:41:12 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:41:12 Jiminny\Jobs\Crm\SyncHubspotObjects ....... 17.23ms DONE
docker_lamp_1 |
docker_lamp_1 | 2026-04-22 15:42:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:42:03 Running ['artisan' dialers:monitor-activities] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:42:05 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:42:06 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:42:07 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:42:09 Running ['artisan' conference:monitor:count] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:42:10 Running ['artisan' mailbox:batch:create] ....... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:create > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 | 2026-04-22 15:42:14 Jiminny\Jobs\Mailbox\CreateBatches [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:42:14 Jiminny\Jobs\Mailbox\CreateBatches [PASSWORD_DOTS] 87.60ms DONE
docker_lamp_1 |
docker_lamp_1 | 2026-04-22 15:43:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:43:04 Running ['artisan' dialers:monitor-activities] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:43:06 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:43:08 Running ['artisan' mailbox:skip-lists:refresh] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:43:10 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:43:11 Running ['artisan' mailbox:batch:retry-failed --max-batches=15] in background 5.15ms DONE
docker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' mailbox:batch:retry-failed --max-batches=15 > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish "framework/schedule-390defd641effba0f73a895e426ded4cf2ba7f11" "$?") > '/dev/null' 2>&1 &
docker_lamp_1 | 2026-04-22 15:43:11 Running ['artisan' calendar:sync --dateMode=daily] 2026-04-22 15:43:19 Jiminny\Jobs\Calendar\SyncCalendarEvents ....... RUNNING
docker_lamp_1 | 2026-04-22 15:43:20 Jiminny\Jobs\Calendar\SyncCalendarEvents ....... 1s DONE
docker_lamp_1 | 2026-04-22 15:43:22 Jiminny\Jobs\Calendar\SyncCalendarEvents ....... RUNNING
docker_lamp_1 | 10s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' calendar:sync --dateMode=daily > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 | 2026-04-22 15:43:23 Jiminny\Jobs\Calendar\SyncCalendarEvents . 596.48ms DONE
docker_lamp_1 | 2026-04-22 15:43:23 Jiminny\Jobs\Calendar\SyncCalendarEvents ....... RUNNING
docker_lamp_1 | 2026-04-22 15:43:23 Jiminny\Jobs\Calendar\SyncCalendarEvents .. 52.11ms DONE
docker_lamp_1 |
docker_lamp_1 | 2026-04-22 15:44:02 Running ['artisan' meeting-bot:schedule-bot] ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:44:04 Running ['artisan' dialers:monitor-activities] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:44:05 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:44:07 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:44:09 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:44:11 Running ['artisan' conference:monitor:count] ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:44:13 Running ['artisan' crm:sync-objects] Syncing objects for Salesforce Team (6473c918-d8db-4ded-a52b-4febfd7b7c02) since 2026-03-19 20:14:10 (delay: 0s)
docker_lamp_1 | Team Edge Communications (c3a725ef-fc5e-4c7b-9231-6379b2834101) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Burlington Textiles Corp of America (72c0ccae-28a5-44bd-a0b4-958e73135463) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Uber (1c523aed-80e3-4c44-b215-7f0b027a03a2) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Vonage (c6030540-121e-4d6b-9261-8d82105817bf) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Nexmo (eaba09e1-4973-4077-b041-ad389248318c) is not yet assigned an owner. skipping...
docker_lamp_1 | Team SF (018e2d7c-ed01-4af2-be33-5c28f454a185) is not yet assigned an owner. skipping...
docker_lamp_1 | Team NewAccount (27325244-25d7-4431-aac4-9ddb2bde6b45) is not yet assigned an owner. skipping...
docker_lamp_1 | Syncing objects for Pipedrive, Inc. (51467630-d89d-480b-be20-933e64a042f7) since 2026-04-08 18:14:13 (delay: 2s)
docker_lamp_1 | Syncing objects for Copper (396ed57c-e3c4-49be-8290-37c32955f7c7) since 2026-04-22 15:14:16 (delay: 4s)
docker_lamp_1 | Syncing objects for Pipedrive External Test (fda3cbdf-1117-4ba5-86f8-775f548b3a28) since 2026-04-08 18:14:16 (delay: 6s)
docker_lamp_1 | Syncing objects for Close (3ff5a02a-86fb-4357-b1d6-a04e26c38602) since 2026-04-22 15:14:20 (delay: 8s)
docker_lamp_1 | Team DeewanyAccount (cfaf90cb-ecf0-432c-a655-a22d9985e4c8) is not yet assigned an owner. skipping...
docker_lamp_1 | Syncing objects for Bullhorn (1640a0ac-19da-4c3b-90f7-87525f07a6d2) since 2026-02-19 13:28:05 (delay: 10s)
docker_lamp_1 | Team Horen test (93d8f8f2-dd61-4137-97de-a02a9d3751bb) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Ahmed Hamadi (191379e4-5e90-4419-8ffd-4804a5abb565) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Ahmed Testing (3b2e1fad-bc5f-4322-84d2-a7440f78c5d7) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Ahmed jiminny (1dbd47b8-bbf8-4bbb-8e28-67b9c91f566e) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Testers Inc (ec0767d5-1248-4447-b316-94dd9a30d52b) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Copper Team (817c8ce1-8e22-4ac8-8fca-8df1131963ab) is not yet assigned an owner. skipping...
docker_lamp_1 | Team TheBestPlace Ever (dbb998f9-70eb-4f11-a9f2-d9f6157f4035) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Horen's apartments (e10bb4ea-6c89-4fe3-be3b-0a278aa81180) is not yet assigned an owner. skipping...
docker_lamp_1 | Team New Org Test (2cb527b6-73a4-46be-b7b2-b0097b494cc5) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Pipedrive test org (18ea22be-6e0b-461c-a4db-536c022aac73) is not yet assigned an owner. skipping...
docker_lamp_1 | Team BigChairs Inc. (16dcfaf9-44b4-4cc2-9ead-f98dd0cad4dd) is not yet assigned an owner. skipping...
docker_lamp_1 | Syncing objects for GL 500 (0c33bf2d-1c77-4200-8ed6-6147ad444c30) since 2026-02-19 13:26:04 (delay: 12s)
docker_lamp_1 | Team Loren X (243d7d4b-1849-46c2-9dfb-4462f52020ef) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Laravel Company 2024 (e466d657-c26d-4281-9ed5-73890fe711f6) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Barbara Fuchs (bea0fcf3-f8f9-471d-92c4-d1b7d1a2e228) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Test salesforce auto sync (2c2e652e-1cbb-4620-852d-e73bef0ab4a9) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Bullhorn (83c14120-cdb1-42e9-b0ac-16ef11320a8f) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Globo Tech (fb8d5211-3c4d-4804-9223-b091670ffd79) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Laravel 11 Company 2024 (780c6836-764b-4e47-9a05-cfefbab6e590) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Nikon7 (e9bff902-edc3-4b0b-89b1-2a9aa43d940a) is not yet assigned an owner. skipping...
docker_lamp_1 | Syncing objects for Dev Zoho CRM client (1ece66c8-feb1-4df1-b321-21607daf4623) since 2026-04-22 15:14:26 (delay: 14s)
docker_lamp_1 | [PASSWORD_DOTS] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:sync-objects > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 | 2026-04-22 15:44:15 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:15 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 49.60ms DONE
docker_lamp_1 | 2026-04-22 15:44:15 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob RUNNING
docker_lamp_1 | 2026-04-22 15:44:16 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob 601.82ms DONE
docker_lamp_1 | 2026-04-22 15:44:17 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:17 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 24.34ms DONE
docker_lamp_1 | 2026-04-22 15:44:19 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:20 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 1s DONE
docker_lamp_1 | 2026-04-22 15:44:20 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob RUNNING
docker_lamp_1 | 2026-04-22 15:44:21 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob 557.11ms DONE
docker_lamp_1 | 2026-04-22 15:44:21 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:21 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 58.10ms DONE
docker_lamp_1 | 2026-04-22 15:44:23 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:24 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 1s DONE
docker_lamp_1 | 2026-04-22 15:44:25 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob RUNNING
docker_lamp_1 | 2026-04-22 15:44:26 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob 787.71ms DONE
docker_lamp_1 | 2026-04-22 15:44:26 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:26 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 18.07ms DONE
docker_lamp_1 | 2026-04-22 15:44:27 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:27 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 19.18ms DONE
docker_lamp_1 | 2026-04-22 15:44:29 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:33 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 3s DONE
docker_lamp_1 | 2026-04-22 15:44:33 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob RUNNING
docker_lamp_1 | 2026-04-22 15:44:34 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob 848.26ms DONE
docker_lamp_1 | 2026-04-22 15:44:35 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob RUNNING
docker_lamp_1 | 2026-04-22 15:44:35 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob 431.28ms DONE
docker_lamp_1 |
docker_lamp_1 | 2026-04-22 15:45:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:03 Running ['artisan' dialers:monitor-activities] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:06 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:09 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:10 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:12 Running ['artisan' activity:purge-stale] ....... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:purge-stale > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:15 Running ['artisan' mailbox:text-relay:sync] {
docker_lamp_1 | "error": "invalid_request",
docker_lamp_1 | "error_description": "Invalid impersonation \u0026quot;sub\u0026quot; field: @"
docker_lamp_1 | }
docker_lamp_1 | .... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:text-relay:sync > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:17 Running ['artisan' conference:pre-meeting-notification] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:pre-meeting-notification > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:19 Running ['artisan' conference:monitor:start] ... 4s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:start > '/proc/1/fd/1' 2>&1
unexpected EOF
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/infrastructure/dev/docker (develop) $ work
WARN[0000] /Users/lukas/jiminny/infrastructure/dev/docker/docker-compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion
Attaching to blackfire-1, datadog-1, jiminny_ext-1, mariadb-1, docker_lamp_1, elasticsearch, kibana, ngrok, redis
redis | 1:C 23 Apr 2026 07:55:51.731 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
redis | 1:C 23 Apr 2026 07:55:51.731 # Redis version=5.0.14, bits=64, commit=00000000, modified=0, pid=1, just started
redis | 1:C 23 Apr 2026 07:55:51.731 # Configuration loaded
mariadb-1 | 2026-04-23 07:55:51+00:00 [Note] [Entrypoint]: Entrypoint script for MariaDB Server 1:11.4.5+maria~ubu2404 started.
redis | 1:M 23 Apr 2026 07:55:51.741 * Running mode=standalone, port=6379.
redis | 1:M 23 Apr 2026 07:55:51.741 # Server initialized
redis | 1:M 23 Apr 2026 07:55:51.741 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled.
redis | 1:M 23 Apr 2026 07:55:51.744 * Reading RDB preamble from AOF file...
redis | 1:M 23 Apr 2026 07:55:51.745 * Reading the remaining AOF tail...
blackfire-1 | [2026-04-23T07:55:51Z] ERROR: The server ID parameter is not set. Please run 'blackfire-agent -register' to configure it.
blackfire-1 | usage blackfire-agent [options]
blackfire-1 | --collector="[URL_WITH_CREDENTIALS] controller (64 bit): Version 7.10.2 (Build 40a3af639d4698) Copyright (c) 2020 Elasticsearch BV" }
elasticsearch | {"type": "server", "timestamp": "2026-04-23T07:56:08,335Z", "level": "INFO", "component": "o.e.t.NettyAllocator", "cluster.name": "docker-cluster", "node.name": "e802ad473a4f", "message": "creating NettyAllocator with the following configs: [name=unpooled, suggested_max_allocation_size=256kb, factors={es.unsafe.use_unpooled_allocator=null, g1gc_enabled=true, g1gc_region_size=1mb, heap_size=700mb}]" }
elasticsearch | {"type": "server", "timestamp": "2026-04-23T07:56:08,444Z", "level": "INFO", "component": "o.e.d.DiscoveryModule", "cluster.name": "docker-cluster", "node.name": "e802ad473a4f", "message": "using discovery type [single-node] and seed hosts providers [settings]" }
elasticsearch | {"type": "server", "timestamp": "2026-04-23T07:56:08,962Z", "level": "WARN", "component": "o.e.g.DanglingIndicesState", "cluster.name": "docker-cluster", "node.name": "e802ad473a4f", "message": "gateway.auto_import_dangling_indices is disabled, dangling indices will not be automatically detected or imported and must be managed manually" }
elasticsearch | {"type": "server", "timestamp": "2026-04-23T07:56:09,262Z", "level": "INFO", "component": "o.e.n.Node", "cluster.name": "docker-cluster", "node.name": "e802ad473a4f", "message": "initialized" }
elasticsearch | {"type": "server", "timestamp": "2026-04-23T07:56:09,263Z", "level": "INFO", "component": "o.e.n.Node", "cluster.name": "docker-cluster", "node.name": "e802ad473a4f", "message": "starting ..." }
elasticsearch | {"type": "server", "timestamp": "2026-04-23T07:56:09,430Z", "level": "INFO", "component": "o.e.t.TransportService", "cluster.name": "docker-cluster", "node.name...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"0 social account(s) to be processed ...\ndocker_lamp_1 | \ndocker_lamp_1 | Done!\ndocker_lamp_1 | 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:bullhorn:ping --heartbeat > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-04-22 15:41:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:41:04 Running ['artisan' dialers:monitor-activities] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:41:06 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:41:07 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:41:08 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:41:10 Running ['artisan' crm:sync-hubspot-objects] [HubSpot] Syncing objects for Hubspot (abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4) since 2026-04-22 15:36:20 (delay: 0s)\ndocker_lamp_1 | Team TestV (2708d27a-7f31-4d90-bd2b-b5bde1c0211e) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team KioskAccount (dedab245-604c-4cd2-a83e-4b9e034b2772) is not yet assigned an owner. skipping...\ndocker_lamp_1 | [HubSpot] Syncing objects for InsightSquared (b2b115eb-93ce-4d1b-929c-173757df8fba) since 2026-02-17 15:09:59 (delay: 0s)\ndocker_lamp_1 | Team Aircall Demo (7980e5eb-b11c-4cee-9c21-8bb29ba85f3b) is not yet assigned an owner. skipping...\ndocker_lamp_1 | [HubSpot] Syncing objects for GoStudent UAT (b2d49a54-b645-4637-a7ae-a86cfce6e8e4) since 2026-02-17 15:09:59 (delay: 1s)\ndocker_lamp_1 | [HubSpot] Syncing objects for JustCall (c6b9d6b0-b48d-4832-a68c-a57d60651888) since 2026-02-17 15:07:41 (delay: 1s)\ndocker_lamp_1 | Team Twilio Video (c334ca55-b230-411c-b10e-31c8204bd07b) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team My Test Account 3000 (dbc9990d-b35f-4e38-9550-22cdd6059514) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team test (7997eb70-8aa4-491a-870d-311977568df4) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Test (5e06dcee-0613-470e-9a77-2c283198f3bf) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team test ogg auto sync (da44776e-306f-427a-83d8-a1b4baa5537e) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Tourlaner (d9b71080-388b-4cf5-8175-aa0f29bee635) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Dispatched 4 HubSpot sync jobs\ndocker_lamp_1 | 2026-04-22 15:41:11 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:sync-hubspot-objects > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | 2026-04-22 15:41:11 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ....... 44.08ms DONE\ndocker_lamp_1 | 2026-04-22 15:41:11 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | 2026-04-22 15:41:12 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ...... 577.01ms DONE\ndocker_lamp_1 | 2026-04-22 15:41:12 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | 2026-04-22 15:41:12 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ....... 16.42ms DONE\ndocker_lamp_1 | 2026-04-22 15:41:12 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | 2026-04-22 15:41:12 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ....... 17.23ms DONE\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-04-22 15:42:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:42:03 Running ['artisan' dialers:monitor-activities] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:42:05 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:42:06 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:42:07 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:42:09 Running ['artisan' conference:monitor:count] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:42:10 Running ['artisan' mailbox:batch:create] ....... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:create > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | 2026-04-22 15:42:14 Jiminny\\Jobs\\Mailbox\\CreateBatches ............. RUNNING\ndocker_lamp_1 | 2026-04-22 15:42:14 Jiminny\\Jobs\\Mailbox\\CreateBatches ........ 87.60ms DONE\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-04-22 15:43:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:43:04 Running ['artisan' dialers:monitor-activities] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:43:06 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:43:08 Running ['artisan' mailbox:skip-lists:refresh] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:43:10 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:43:11 Running ['artisan' mailbox:batch:retry-failed --max-batches=15] in background 5.15ms DONE\ndocker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' mailbox:batch:retry-failed --max-batches=15 > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish \"framework/schedule-390defd641effba0f73a895e426ded4cf2ba7f11\" \"$?\") > '/dev/null' 2>&1 & \ndocker_lamp_1 | 2026-04-22 15:43:11 Running ['artisan' calendar:sync --dateMode=daily] 2026-04-22 15:43:19 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents ....... RUNNING\ndocker_lamp_1 | 2026-04-22 15:43:20 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents ....... 1s DONE\ndocker_lamp_1 | 2026-04-22 15:43:22 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents ....... RUNNING\ndocker_lamp_1 | 10s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' calendar:sync --dateMode=daily > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | 2026-04-22 15:43:23 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents . 596.48ms DONE\ndocker_lamp_1 | 2026-04-22 15:43:23 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents ....... RUNNING\ndocker_lamp_1 | 2026-04-22 15:43:23 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents .. 52.11ms DONE\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-04-22 15:44:02 Running ['artisan' meeting-bot:schedule-bot] ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:44:04 Running ['artisan' dialers:monitor-activities] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:44:05 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:44:07 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:44:09 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:44:11 Running ['artisan' conference:monitor:count] ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:44:13 Running ['artisan' crm:sync-objects] Syncing objects for Salesforce Team (6473c918-d8db-4ded-a52b-4febfd7b7c02) since 2026-03-19 20:14:10 (delay: 0s)\ndocker_lamp_1 | Team Edge Communications (c3a725ef-fc5e-4c7b-9231-6379b2834101) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Burlington Textiles Corp of America (72c0ccae-28a5-44bd-a0b4-958e73135463) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Uber (1c523aed-80e3-4c44-b215-7f0b027a03a2) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Vonage (c6030540-121e-4d6b-9261-8d82105817bf) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Nexmo (eaba09e1-4973-4077-b041-ad389248318c) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team SF (018e2d7c-ed01-4af2-be33-5c28f454a185) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team NewAccount (27325244-25d7-4431-aac4-9ddb2bde6b45) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Syncing objects for Pipedrive, Inc. (51467630-d89d-480b-be20-933e64a042f7) since 2026-04-08 18:14:13 (delay: 2s)\ndocker_lamp_1 | Syncing objects for Copper (396ed57c-e3c4-49be-8290-37c32955f7c7) since 2026-04-22 15:14:16 (delay: 4s)\ndocker_lamp_1 | Syncing objects for Pipedrive External Test (fda3cbdf-1117-4ba5-86f8-775f548b3a28) since 2026-04-08 18:14:16 (delay: 6s)\ndocker_lamp_1 | Syncing objects for Close (3ff5a02a-86fb-4357-b1d6-a04e26c38602) since 2026-04-22 15:14:20 (delay: 8s)\ndocker_lamp_1 | Team DeewanyAccount (cfaf90cb-ecf0-432c-a655-a22d9985e4c8) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Syncing objects for Bullhorn (1640a0ac-19da-4c3b-90f7-87525f07a6d2) since 2026-02-19 13:28:05 (delay: 10s)\ndocker_lamp_1 | Team Horen test (93d8f8f2-dd61-4137-97de-a02a9d3751bb) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Ahmed Hamadi (191379e4-5e90-4419-8ffd-4804a5abb565) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Ahmed Testing (3b2e1fad-bc5f-4322-84d2-a7440f78c5d7) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Ahmed jiminny (1dbd47b8-bbf8-4bbb-8e28-67b9c91f566e) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Testers Inc (ec0767d5-1248-4447-b316-94dd9a30d52b) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Copper Team (817c8ce1-8e22-4ac8-8fca-8df1131963ab) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team TheBestPlace Ever (dbb998f9-70eb-4f11-a9f2-d9f6157f4035) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Horen's apartments (e10bb4ea-6c89-4fe3-be3b-0a278aa81180) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team New Org Test (2cb527b6-73a4-46be-b7b2-b0097b494cc5) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Pipedrive test org (18ea22be-6e0b-461c-a4db-536c022aac73) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team BigChairs Inc. (16dcfaf9-44b4-4cc2-9ead-f98dd0cad4dd) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Syncing objects for GL 500 (0c33bf2d-1c77-4200-8ed6-6147ad444c30) since 2026-02-19 13:26:04 (delay: 12s)\ndocker_lamp_1 | Team Loren X (243d7d4b-1849-46c2-9dfb-4462f52020ef) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Laravel Company 2024 (e466d657-c26d-4281-9ed5-73890fe711f6) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Barbara Fuchs (bea0fcf3-f8f9-471d-92c4-d1b7d1a2e228) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Test salesforce auto sync (2c2e652e-1cbb-4620-852d-e73bef0ab4a9) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Bullhorn (83c14120-cdb1-42e9-b0ac-16ef11320a8f) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Globo Tech (fb8d5211-3c4d-4804-9223-b091670ffd79) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Laravel 11 Company 2024 (780c6836-764b-4e47-9a05-cfefbab6e590) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Nikon7 (e9bff902-edc3-4b0b-89b1-2a9aa43d940a) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Syncing objects for Dev Zoho CRM client (1ece66c8-feb1-4df1-b321-21607daf4623) since 2026-04-22 15:14:26 (delay: 14s)\ndocker_lamp_1 | ........... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:sync-objects > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | 2026-04-22 15:44:15 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:15 Jiminny\\Jobs\\Crm\\SyncObjects .............. 49.60ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:15 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:16 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob 601.82ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:17 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:17 Jiminny\\Jobs\\Crm\\SyncObjects .............. 24.34ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:19 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:20 Jiminny\\Jobs\\Crm\\SyncObjects ................... 1s DONE\ndocker_lamp_1 | 2026-04-22 15:44:20 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:21 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob 557.11ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:21 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:21 Jiminny\\Jobs\\Crm\\SyncObjects .............. 58.10ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:23 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:24 Jiminny\\Jobs\\Crm\\SyncObjects ................... 1s DONE\ndocker_lamp_1 | 2026-04-22 15:44:25 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:26 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob 787.71ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:26 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:26 Jiminny\\Jobs\\Crm\\SyncObjects .............. 18.07ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:27 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:27 Jiminny\\Jobs\\Crm\\SyncObjects .............. 19.18ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:29 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:33 Jiminny\\Jobs\\Crm\\SyncObjects ................... 3s DONE\ndocker_lamp_1 | 2026-04-22 15:44:33 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:34 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob 848.26ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:35 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:35 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob 431.28ms DONE\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-04-22 15:45:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:03 Running ['artisan' dialers:monitor-activities] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:06 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:09 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:10 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:12 Running ['artisan' activity:purge-stale] ....... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:purge-stale > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:15 Running ['artisan' mailbox:text-relay:sync] {\ndocker_lamp_1 | \"error\": \"invalid_request\",\ndocker_lamp_1 | \"error_description\": \"Invalid impersonation \\u0026quot;sub\\u0026quot; field: @\"\ndocker_lamp_1 | }\ndocker_lamp_1 | .... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:text-relay:sync > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:17 Running ['artisan' conference:pre-meeting-notification] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:pre-meeting-notification > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:19 Running ['artisan' conference:monitor:start] ... 4s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:start > '/proc/1/fd/1' 2>&1 \nunexpected EOF\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/infrastructure/dev/docker (develop) $ work\nWARN[0000] /Users/lukas/jiminny/infrastructure/dev/docker/docker-compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion \nAttaching to blackfire-1, datadog-1, jiminny_ext-1, mariadb-1, docker_lamp_1, elasticsearch, kibana, ngrok, redis\nredis | 1:C 23 Apr 2026 07:55:51.731 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo\nredis | 1:C 23 Apr 2026 07:55:51.731 # Redis version=5.0.14, bits=64, commit=00000000, modified=0, pid=1, just started\nredis | 1:C 23 Apr 2026 07:55:51.731 # Configuration loaded\nmariadb-1 | 2026-04-23 07:55:51+00:00 [Note] [Entrypoint]: Entrypoint script for MariaDB Server 1:11.4.5+maria~ubu2404 started.\nredis | 1:M 23 Apr 2026 07:55:51.741 * Running mode=standalone, port=6379.\nredis | 1:M 23 Apr 2026 07:55:51.741 # Server initialized\nredis | 1:M 23 Apr 2026 07:55:51.741 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled.\nredis | 1:M 23 Apr 2026 07:55:51.744 * Reading RDB preamble from AOF file...\nredis | 1:M 23 Apr 2026 07:55:51.745 * Reading the remaining AOF tail...\nblackfire-1 | [2026-04-23T07:55:51Z] ERROR: The server ID parameter is not set. Please run 'blackfire-agent -register' to configure it.\nblackfire-1 | usage blackfire-agent [options]\nblackfire-1 | --collector=\"https://blackfire.io\": Sets the URL of Blackfire's data collector\nblackfire-1 | --config=\"/etc/blackfire/agent\": Sets the path to the configuration file\nblackfire-1 | -d: Prints the current configuration\nblackfire-1 | --http-proxy=\"\": Sets the HTTP proxy to use\nblackfire-1 | --https-proxy=\"\": Sets the HTTPS proxy to use\nblackfire-1 | --log-file=\"stderr\": Sets the path of the log file. Use stderr to log to stderr\nblackfire-1 | --log-level=\"1\": log verbosity level (4: debug, 3: info, 2: warning, 1: error)\nblackfire-1 | --register: Helps you with registering the agent\nblackfire-1 | --server-id=\"\": Sets the server id used to authenticate with Blackfire API\nblackfire-1 | --server-token=\"\": Sets the server token used to authenticate with Blackfire API. It is unsafe to set this from the command line\nblackfire-1 | --socket=\"unix:///var/run/blackfire/agent.sock\": Sets the socket the agent should read traces from. Possible value can be a unix socket or a TCP address. ie: unix:///var/run/blackfire/agent.sock or tcp://127.0.0.1:8307\nblackfire-1 | --test: Tests the configuration\nblackfire-1 | --timeout=\"15s\": Sets the Blackfire connection timeout\nblackfire-1 | -v: Prints the version number\nngrok | t=2026-04-23T07:55:52+0000 lvl=info msg=\"no configuration paths supplied\"\nngrok | t=2026-04-23T07:55:52+0000 lvl=info msg=\"using configuration at default config path\" path=/home/ngrok/.ngrok2/ngrok.yml\nngrok | t=2026-04-23T07:55:52+0000 lvl=info msg=\"open config file\" path=/home/ngrok/.ngrok2/ngrok.yml err=nil\nblackfire-1 exited with code 1\njiminny_ext-1 exited with code 0\nngrok | t=2026-04-23T07:55:52+0000 lvl=info msg=\"starting web service\" obj=web addr=0.0.0.0:4040\ndocker_lamp_1 | + main\ndocker_lamp_1 | + declare START_DIR\ndocker_lamp_1 | +++ realpath /scripts/init-dev\ndocker_lamp_1 | ++ dirname /scripts/init-dev\ndocker_lamp_1 | + START_DIR=/scripts\ndocker_lamp_1 | + readonly START_DIR\ndocker_lamp_1 | + source /scripts/storage_init.sh\ndocker_lamp_1 | ++ set -o errexit\ndocker_lamp_1 | ++ set -o nounset\ndocker_lamp_1 | ++ set -o pipefail\ndocker_lamp_1 | + create_bind_mount\ndocker_lamp_1 | + [[ 0 == \\1 ]]\ndocker_lamp_1 | + configure_xdebug\ndocker_lamp_1 | + j2 /root/.j2_templates/xdebug/xdebug.ini.j2\nmariadb-1 | 2026-04-23 07:55:52+00:00 [Warn] [Entrypoint]: /sys/fs/cgroup///memory.pressure not writable, functionality unavailable to MariaDB\nmariadb-1 | 2026-04-23 07:55:52+00:00 [Note] [Entrypoint]: Switching to dedicated user 'mysql'\nmariadb-1 | 2026-04-23 07:55:52+00:00 [Note] [Entrypoint]: Entrypoint script for MariaDB Server 1:11.4.5+maria~ubu2404 started.\ndatadog-1 | [s6-init] making user provided files available at /var/run/s6/etc...exited 0.\nngrok | t=2026-04-23T07:55:52+0000 lvl=info msg=\"tunnel session started\" obj=tunnels.session\nngrok | t=2026-04-23T07:55:52+0000 lvl=info msg=\"client session established\" obj=csess id=1c8f9eb905ac\nmariadb-1 | 2026-04-23 07:55:53+00:00 [Note] [Entrypoint]: MariaDB upgrade information missing, assuming required\nmariadb-1 | 2026-04-23 07:55:53+00:00 [Note] [Entrypoint]: MariaDB upgrade (mariadb-upgrade or creating healthcheck users) required, but skipped due to $MARIADB_AUTO_UPGRADE setting\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] Starting MariaDB 11.4.5-MariaDB-ubu2404 source revision 0771110266ff5c04216af4bf1243c65f8c67ccf4 server_uid zs02SyAxkg7epm72pZgde8TQHsk= as process 1\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Compressed tables use zlib 1.3\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Number of transaction pools: 1\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Using ARMv8 crc32 + pmull instructions\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Using liburing\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Initializing buffer pool, total size = 128.000MiB, chunk size = 2.000MiB\nngrok | t=2026-04-23T07:55:53+0000 lvl=info msg=\"started tunnel\" obj=tunnels name=\"command_line (http)\" addr=http://lamp:3080 url=http://lukask.ngrok.io\nngrok | t=2026-04-23T07:55:53+0000 lvl=info msg=\"started tunnel\" obj=tunnels name=command_line addr=http://lamp:3080 url=https://lukask.ngrok.io\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Completed initialization of buffer pool\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: File system buffers for log disabled (block size=512 bytes)\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: End of log at LSN=7618260638\ndocker_lamp_1 | + configure_blackfire\ndocker_lamp_1 | + j2 /root/.j2_templates/blackfire/extension.ini.j2\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Opened 3 undo tablespaces\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: 128 rollback segments in 3 undo tablespaces are active.\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Setting file './ibtmp1' size to 12.000MiB. Physically writing the file full; Please wait ...\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: File './ibtmp1' size is now 12.000MiB.\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: log sequence number 7618260638; transaction id 11490938\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] Plugin 'FEEDBACK' is disabled.\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] Plugin 'wsrep-provider' is disabled.\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Loading buffer pool(s) from /var/lib/mysql/ib_buffer_pool\ndocker_lamp_1 | + j2 /root/.j2_templates/blackfire/cli.ini.j2\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Buffer pool(s) load completed at 260423 7:55:53\nngrok | t=2026-04-23T07:55:53+0000 lvl=info msg=\"update available\" obj=updater\ndocker_lamp_1 | + declare EMPTY_DB\ndocker_lamp_1 | + db_is_empty\ndocker_lamp_1 | ++ find /var/lib/mysql/ -maxdepth 1\ndocker_lamp_1 | ++ wc -l\ndocker_lamp_1 | + [[ 11 -lt 5 ]]\ndocker_lamp_1 | + EMPTY_DB=0\ndocker_lamp_1 | + readonly EMPTY_DB\ndocker_lamp_1 | + [[ 0 -eq 1 ]]\ndocker_lamp_1 | + [[ local == \\l\\o\\c\\a\\l ]]\ndocker_lamp_1 | + set_nginx_domain dev.jiminny.com\ndocker_lamp_1 | + declare -r DOMAIN_NAME=dev.jiminny.com\ndocker_lamp_1 | + cp -f /etc/nginx/nginx_template.conf /etc/nginx/nginx.conf\ndocker_lamp_1 | + sed -i -E s~_JIMINNY_DOMAIN~app.dev.jiminny.com~g /etc/nginx/nginx.conf\ndocker_lamp_1 | + sed -i -E s~_JIMINNY_EXT_DOMAIN~ext.dev.jiminny.com~g /etc/nginx/nginx.conf\ndocker_lamp_1 | + sed -i -E s~_JIMINNY_WEB_DOMAIN~www.dev.jiminny.com~g /etc/nginx/nginx.conf\ndocker_lamp_1 | + [[ -n 3399 ]]\ndocker_lamp_1 | + sed -i -E 's~http:\\/\\/jiminny_ext:8080~http://jiminny_ext:3399~g' /etc/nginx/nginx.conf\ndocker_lamp_1 | + [[ -n host.docker.internal ]]\ndocker_lamp_1 | + sed -i -E 's~http:\\/\\/jiminny_ext~http://host.docker.internal~g' /etc/nginx/nginx.conf\ndocker_lamp_1 | + build_dev\ndocker_lamp_1 | + cd /home/jiminny/\ndocker_lamp_1 | + create_dot_env_local_file\ndocker_lamp_1 | + cp -f /home/jiminny/.env.local /home/jiminny/.env.local.bak\ndocker_lamp_1 | + create_dot_env\ndocker_lamp_1 | + [[ -f /home/jiminny/.env ]]\ndocker_lamp_1 | + return\ndocker_lamp_1 | + declare DB_ADMIN_PASSWORD\ndocker_lamp_1 | + declare DB_ADMIN_USERNAME\ndocker_lamp_1 | + declare DB_DEV_PASSWORD\ndocker_lamp_1 | + declare DB_DEV_USERNAME\ndocker_lamp_1 | + declare DB_ROOT_PASSWORD\ndocker_lamp_1 | + declare DB_ROOT_USERNAME\ndocker_lamp_1 | + declare DB_WEB_PASSWORD\ndocker_lamp_1 | + declare DB_WEB_USERNAME\ndocker_lamp_1 | ++ jq -r .DB_ADMIN_PASSWORD /home/jiminny/dev.json\ndatadog-1 | [s6-init] ensuring user provided files have correct perms...exited 0.\ndocker_lamp_1 | + DB_ADMIN_PASSWORD='dgyt$rTe21-d'\ndocker_lamp_1 | ++ jq -r .DB_ADMIN_USERNAME /home/jiminny/dev.json\ndatadog-1 | [fix-attrs.d] applying ownership & permissions fixes...\ndocker_lamp_1 | + DB_ADMIN_USERNAME=jmnyadmin\ndocker_lamp_1 | ++ jq -r .DB_DEV_PASSWORD /home/jiminny/dev.json\ndatadog-1 | [fix-attrs.d] done.\ndocker_lamp_1 | + DB_DEV_PASSWORD=rTr4sdQA65-Ad\n\n\ndatadog-1 | [cont-init.d] executing container initialization scripts...\ndocker_lamp_1 | ++ jq -r .DB_DEV_USERNAME /home/jiminny/dev.json\ndocker_lamp_1 | + DB_DEV_USERNAME=jmnydev\ndocker_lamp_1 | ++ jq -r .DB_ROOT_PASSWORD /home/jiminny/dev.json\ndatadog-1 | [cont-init.d] 01-check-apikey.sh: executing... \ndocker_lamp_1 | + DB_ROOT_PASSWORD=b7h5-1fH3e54J\ndocker_lamp_1 | ++ jq -r .DB_ROOT_USERNAME /home/jiminny/dev.json\ndocker_lamp_1 | + DB_ROOT_USERNAME=root\ndocker_lamp_1 | ++ jq -r .DB_WEB_PASSWORD /home/jiminny/dev.json\ndatadog-1 | \ndatadog-1 | ==================================================================================\ndatadog-1 | You must set an DD_API_KEY environment variable to run the Datadog Agent container\ndatadog-1 | ==================================================================================\ndatadog-1 | \ndatadog-1 | [cont-init.d] 01-check-apikey.sh: exited 1.\ndocker_lamp_1 | + DB_WEB_PASSWORD=aR5-EWf23b8da\ndocker_lamp_1 | ++ jq -r .DB_WEB_USERNAME /home/jiminny/dev.json\ndocker_lamp_1 | + DB_WEB_USERNAME=jmnyweb\ndocker_lamp_1 | + readonly DB_ADMIN_PASSWORD\ndocker_lamp_1 | + readonly DB_ADMIN_USERNAME\ndocker_lamp_1 | + readonly DB_DEV_PASSWORD\ndocker_lamp_1 | + readonly DB_DEV_USERNAME\ndocker_lamp_1 | + readonly DB_ROOT_PASSWORD\ndocker_lamp_1 | + readonly DB_ROOT_USERNAME\ndocker_lamp_1 | + readonly DB_WEB_PASSWORD\ndocker_lamp_1 | + readonly DB_WEB_USERNAME\ndocker_lamp_1 | + sed -i -E 's~DB_USERNAME=.*$~DB_USERNAME=jmnyadmin~g' /home/jiminny/.env\ndocker_lamp_1 | + sed -i -E 's~DB_PASSWORD=.*$~DB_PASSWORD=dgyt$rTe21-d~g' /home/jiminny/.env\ndocker_lamp_1 | + sed -i -E 's~DB_HOST=.*$~DB_HOST=mariadb~g' /home/jiminny/.env\ndocker_lamp_1 | + cp -f /home/jiminny/.env /home/jiminny/.env.root\ndocker_lamp_1 | + cp -f /home/jiminny/.env /home/jiminny/.env.migrate\ndocker_lamp_1 | + sed -i -E 's~DB_PASSWORD=.*$~DB_PASSWORD=b7h5-1fH3e54J~g' /home/jiminny/.env.migrate\ndocker_lamp_1 | + sed -i -E 's~DB_PASSWORD=.*$~DB_PASSWORD=b7h5-1fH3e54J~g' /home/jiminny/.env.root\ndocker_lamp_1 | + sed -i -E 's~DB_USERNAME=.*$~DB_USERNAME=root~g' /home/jiminny/.env.migrate\ndocker_lamp_1 | + sed -i -E 's~DB_USERNAME=.*$~DB_USERNAME=root~g' /home/jiminny/.env.root\ndocker_lamp_1 | + cp -f /home/jiminny/.env /home/jiminny/.env.local\ndocker_lamp_1 | + echo ''\ndocker_lamp_1 | + echo 'DB_ADMIN_PASSWORD=dgyt$rTe21-d'\ndocker_lamp_1 | + echo DB_ADMIN_USERNAME=jmnyadmin\ndocker_lamp_1 | + echo DB_DEV_PASSWORD=rTr4sdQA65-Ad\ndocker_lamp_1 | + echo DB_DEV_USERNAME=jmnydev\ndocker_lamp_1 | + echo DB_ROOT_PASSWORD=b7h5-1fH3e54J\ndocker_lamp_1 | + echo DB_ROOT_USERNAME=root\ndocker_lamp_1 | + echo DB_WEB_PASSWORD=aR5-EWf23b8da\ndocker_lamp_1 | + echo DB_WEB_USERNAME=jmnyweb\ndocker_lamp_1 | + [[ false == \\f\\a\\l\\s\\e ]]\ndocker_lamp_1 | + declare COMPOSER_PARAM=--prefer-dist\ndocker_lamp_1 | + [[ 0 -eq 1 ]]\ndocker_lamp_1 | + composer install --prefer-dist\ndatadog-1 exited with code 1\nmariadb-1 | 2026-04-23 7:55:54 0 [Note] Server socket created on IP: '0.0.0.0'.\nmariadb-1 | 2026-04-23 7:55:54 0 [Note] Server socket created on IP: '::'.\nmariadb-1 | 2026-04-23 7:55:54 0 [Note] mariadbd: Event Scheduler: Loaded 0 events\nmariadb-1 | 2026-04-23 7:55:54 0 [Note] mariadbd: ready for connections.\nmariadb-1 | Version: '11.4.5-MariaDB-ubu2404' socket: '/run/mysqld/mysqld.sock' port: 3306 mariadb.org binary distribution\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:56,514Z\", \"level\": \"INFO\", \"component\": \"o.e.n.Node\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"version[7.10.2], pid[7], build[default/docker/747e1cc71def077253878a59143c1f785afa92b9/2021-01-13T04:42:47.157277Z], OS[Linux/6.12.54-linuxkit/aarch64], JVM[AdoptOpenJDK/OpenJDK 64-Bit Server VM/15.0.1/15.0.1+9]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:56,517Z\", \"level\": \"INFO\", \"component\": \"o.e.n.Node\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"JVM home [/usr/share/elasticsearch/jdk], using bundled JDK [true]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:56,518Z\", \"level\": \"INFO\", \"component\": \"o.e.n.Node\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"JVM arguments [-Xshare:auto, -Des.networkaddress.cache.ttl=60, -Des.networkaddress.cache.negative.ttl=10, -XX:+AlwaysPreTouch, -Xss1m, -Djava.awt.headless=true, -Dfile.encoding=UTF-8, -Djna.nosys=true, -XX:-OmitStackTraceInFastThrow, -XX:+ShowCodeDetailsInExceptionMessages, -Dio.netty.noUnsafe=true, -Dio.netty.noKeySetOptimization=true, -Dio.netty.recycler.maxCapacityPerThread=0, -Dio.netty.allocator.numDirectArenas=0, -Dlog4j.shutdownHookEnabled=false, -Dlog4j2.disable.jmx=true, -Djava.locale.providers=SPI,COMPAT, -Xms1g, -Xmx1g, -XX:+UseG1GC, -XX:G1ReservePercent=25, -XX:InitiatingHeapOccupancyPercent=30, -Djava.io.tmpdir=/tmp/elasticsearch-10124985765930456581, -XX:+HeapDumpOnOutOfMemoryError, -XX:HeapDumpPath=data, -XX:ErrorFile=logs/hs_err_pid%p.log, -Xlog:gc*,gc+age=trace,safepoint:file=logs/gc.log:utctime,pid,tags:filecount=32,filesize=64m, -Des.cgroups.hierarchy.override=/, -Xms700m, -Xmx700m, -XX:MaxDirectMemorySize=367001600, -Des.path.home=/usr/share/elasticsearch, -Des.path.conf=/usr/share/elasticsearch/config, -Des.distribution.flavor=default, -Des.distribution.type=docker, -Des.bundled_jdk=true]\" }\ndocker_lamp_1 | Installing dependencies from lock file (including require-dev)\ndocker_lamp_1 | Verifying lock file contents can be installed on current platform.\ndocker_lamp_1 | Nothing to install, update or remove\ndocker_lamp_1 | Package doctrine/annotations is abandoned, you should avoid using it. No replacement was suggested.\ndocker_lamp_1 | Package symfony/debug is abandoned, you should avoid using it. Use symfony/error-handler instead.\ndocker_lamp_1 | Generating optimized autoload files\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,062Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [aggs-matrix-stats]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,062Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [analysis-common]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,063Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [constant-keyword]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,063Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [flattened]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,063Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [frozen-indices]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,063Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [ingest-common]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,063Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [ingest-geoip]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,063Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [ingest-user-agent]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,063Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [kibana]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,064Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [lang-expression]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,064Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [lang-mustache]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [lang-painless]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [mapper-extras]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [mapper-version]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [parent-join]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [percolator]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [rank-eval]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [reindex]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [repositories-metering-api]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [repository-url]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [search-business-rules]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [searchable-snapshots]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [spatial]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [transform]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [transport-netty4]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [unsigned-long]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [vectors]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [wildcard]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-analytics]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-async]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-async-search]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-autoscaling]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-ccr]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-core]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-data-streams]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-deprecation]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-enrich]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-eql]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-graph]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-identity-provider]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-ilm]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-logstash]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-ml]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-monitoring]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-ql]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-rollup]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-security]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-sql]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,068Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-stack]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,068Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-voting-only-node]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,068Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-watcher]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,068Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"no plugins loaded\" }\nelasticsearch | {\"type\": \"deprecation\", \"timestamp\": \"2026-04-23T07:55:59,122Z\", \"level\": \"DEPRECATION\", \"component\": \"o.e.d.c.s.Settings\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"[node.data] setting was deprecated in Elasticsearch and will be removed in a future release! See the breaking changes documentation for the next major version.\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,139Z\", \"level\": \"INFO\", \"component\": \"o.e.e.NodeEnvironment\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"using [1] data paths, mounts [[/usr/share/elasticsearch/data (/dev/vda1)]], net usable_space [14.5gb], net total_space [58.3gb], types [ext4]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,139Z\", \"level\": \"INFO\", \"component\": \"o.e.e.NodeEnvironment\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"heap size [700mb], compressed ordinary object pointers [true]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,263Z\", \"level\": \"INFO\", \"component\": \"o.e.n.Node\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"node name [e802ad473a4f], node ID [e2ZKzgw4Q4aCf2w5ljWr1A], cluster name [docker-cluster], roles [transform, master, remote_cluster_client, data, ml, data_content, data_hot, data_warm, data_cold, ingest]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:06,887Z\", \"level\": \"INFO\", \"component\": \"o.e.x.m.p.l.CppLogMessageHandler\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"[controller/214] [Main.cc@114] controller (64 bit): Version 7.10.2 (Build 40a3af639d4698) Copyright (c) 2020 Elasticsearch BV\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:08,335Z\", \"level\": \"INFO\", \"component\": \"o.e.t.NettyAllocator\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"creating NettyAllocator with the following configs: [name=unpooled, suggested_max_allocation_size=256kb, factors={es.unsafe.use_unpooled_allocator=null, g1gc_enabled=true, g1gc_region_size=1mb, heap_size=700mb}]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:08,444Z\", \"level\": \"INFO\", \"component\": \"o.e.d.DiscoveryModule\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"using discovery type [single-node] and seed hosts providers [settings]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:08,962Z\", \"level\": \"WARN\", \"component\": \"o.e.g.DanglingIndicesState\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"gateway.auto_import_dangling_indices is disabled, dangling indices will not be automatically detected or imported and must be managed manually\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:09,262Z\", \"level\": \"INFO\", \"component\": \"o.e.n.Node\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"initialized\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:09,263Z\", \"level\": \"INFO\", \"component\": \"o.e.n.Node\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"starting ...\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:09,430Z\", \"level\": \"INFO\", \"component\": \"o.e.t.TransportService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"publish_address {172.18.0.2:9300}, bound_addresses {[::]:9300}\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:09,769Z\", \"level\": \"INFO\", \"component\": \"o.e.c.c.Coordinator\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"cluster UUID [8uh2w1CUSGyWYR_OvaKx6g]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:09,886Z\", \"level\": \"INFO\", \"component\": \"o.e.c.s.MasterService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"elected-as-master ([1] nodes joined)[{e802ad473a4f}{e2ZKzgw4Q4aCf2w5ljWr1A}{cv7PSrNNRgOBYzeY8N23MA}{172.18.0.2}{172.18.0.2:9300}{cdhilmrstw}{ml.machine_memory=4109217792, xpack.installed=true, transform.node=true, ml.max_open_jobs=20} elect leader, _BECOME_MASTER_TASK_, _FINISH_ELECTION_], term: 229, version: 8527, delta: master node changed {previous [], current [{e802ad473a4f}{e2ZKzgw4Q4aCf2w5ljWr1A}{cv7PSrNNRgOBYzeY8N23MA}{172.18.0.2}{172.18.0.2:9300}{cdhilmrstw}{ml.machine_memory=4109217792, xpack.installed=true, transform.node=true, ml.max_open_jobs=20}]}\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:09,973Z\", \"level\": \"INFO\", \"component\": \"o.e.c.s.ClusterApplierService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"master node changed {previous [], current [{e802ad473a4f}{e2ZKzgw4Q4aCf2w5ljWr1A}{cv7PSrNNRgOBYzeY8N23MA}{172.18.0.2}{172.18.0.2:9300}{cdhilmrstw}{ml.machine_memory=4109217792, xpack.installed=true, transform.node=true, ml.max_open_jobs=20}]}, term: 229, version: 8527, reason: Publication{term=229, version=8527}\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:10,019Z\", \"level\": \"INFO\", \"component\": \"o.e.h.AbstractHttpServerTransport\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"publish_address {172.18.0.2:9200}, bound_addresses {[::]:9200}\", \"cluster.uuid\": \"8uh2w1CUSGyWYR_OvaKx6g\", \"node.id\": \"e2ZKzgw4Q4aCf2w5ljWr1A\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:10,020Z\", \"level\": \"INFO\", \"component\": \"o.e.n.Node\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"started\", \"cluster.uuid\": \"8uh2w1CUSGyWYR_OvaKx6g\", \"node.id\": \"e2ZKzgw4Q4aCf2w5ljWr1A\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:10,388Z\", \"level\": \"INFO\", \"component\": \"o.e.l.LicenseService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"license [85e882e5-5714-4173-a5dd-9baa841494a0] mode [basic] - valid\", \"cluster.uuid\": \"8uh2w1CUSGyWYR_OvaKx6g\", \"node.id\": \"e2ZKzgw4Q4aCf2w5ljWr1A\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:10,396Z\", \"level\": \"INFO\", \"component\": \"o.e.g.GatewayService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"recovered [15] indices into cluster_state\", \"cluster.uuid\": \"8uh2w1CUSGyWYR_OvaKx6g\", \"node.id\": \"e2ZKzgw4Q4aCf2w5ljWr1A\" }\nredis | 1:M 23 Apr 2026 07:56:12.207 * DB loaded from append only file: 20.465 seconds\nredis | 1:M 23 Apr 2026 07:56:12.208 * Ready to accept connections\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:12,454Z\", \"level\": \"INFO\", \"component\": \"o.e.c.r.a.AllocationService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"Cluster health status changed from [RED] to [YELLOW] (reason: [shards started [[activities][2]]]).\", \"cluster.uuid\": \"8uh2w1CUSGyWYR_OvaKx6g\", \"node.id\": \"e2ZKzgw4Q4aCf2w5ljWr1A\" }\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:26Z\",\"tags\":[\"info\",\"plugins-service\"],\"pid\":7,\"message\":\"Plugin \\\"visTypeXy\\\" is disabled.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:26Z\",\"tags\":[\"info\",\"plugins-service\"],\"pid\":7,\"message\":\"Plugin \\\"auditTrail\\\" is disabled.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:26Z\",\"tags\":[\"warning\",\"config\",\"deprecation\"],\"pid\":7,\"message\":\"Config key [monitoring.cluster_alerts.email_notifications.email_address] will be required for email notifications to work in 8.0.\\\"\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:26Z\",\"tags\":[\"info\",\"plugins-system\"],\"pid\":7,\"message\":\"Setting up [96] plugins: [taskManager,licensing,globalSearch,globalSearchProviders,code,usageCollection,xpackLegacy,telemetryCollectionManager,telemetry,telemetryCollectionXpack,kibanaUsageCollection,securityOss,newsfeed,mapsLegacy,kibanaLegacy,translations,share,legacyExport,embeddable,uiActionsEnhanced,expressions,data,home,observability,cloud,console,consoleExtensions,apmOss,searchprofiler,painlessLab,grokdebugger,management,indexPatternManagement,advancedSettings,fileUpload,savedObjects,dashboard,visualizations,visTypeTable,visTypeVega,visTypeTimelion,timelion,features,upgradeAssistant,security,snapshotRestore,enterpriseSearch,encryptedSavedObjects,ingestManager,indexManagement,remoteClusters,crossClusterReplication,indexLifecycleManagement,dashboardMode,beatsManagement,transform,ingestPipelines,maps,licenseManagement,graph,dataEnhanced,visTypeMarkdown,tileMap,regionMap,inputControlVis,visualize,esUiShared,charts,lens,visTypeVislib,visTypeTimeseries,rollup,visTypeTagcloud,visTypeMetric,watcher,discover,discoverEnhanced,savedObjectsManagement,spaces,reporting,lists,eventLog,actions,case,alerts,stackAlerts,triggersActionsUi,ml,securitySolution,infra,monitoring,logstash,apm,uptime,bfetch,canvas]\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"security\",\"config\"],\"pid\":7,\"message\":\"Generating a random key for xpack.security.encryptionKey. To prevent sessions from being invalidated on restart, please set xpack.security.encryptionKey in kibana.yml\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"security\",\"config\"],\"pid\":7,\"message\":\"Session cookies will be transmitted over insecure connections. This is not recommended.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"encryptedSavedObjects\",\"config\"],\"pid\":7,\"message\":\"Generating a random key for xpack.encryptedSavedObjects.encryptionKey. To be able to decrypt encrypted saved objects attributes after restart, please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"ingestManager\"],\"pid\":7,\"message\":\"Fleet APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"reporting\",\"config\"],\"pid\":7,\"message\":\"Generating a random key for xpack.reporting.encryptionKey. To prevent sessions from being invalidated on restart, please set xpack.reporting.encryptionKey in kibana.yml\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"reporting\",\"config\"],\"pid\":7,\"message\":\"Found 'server.host: \\\"0\\\"' in Kibana configuration. This is incompatible with Reporting. To enable Reporting to work, 'xpack.reporting.kibanaServer.hostname: 0.0.0.0' is being automatically to the configuration. You can change the setting to 'server.host: 0.0.0.0' or add 'xpack.reporting.kibanaServer.hostname: 0.0.0.0' in kibana.yml to prevent this message.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"reporting\",\"config\"],\"pid\":7,\"message\":\"Chromium sandbox provides an additional layer of protection, but is not supported for Linux CentOS 8.3.2011\\n OS. Automatically setting 'xpack.reporting.capture.browser.chromium.disableSandbox: true'.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"actions\",\"actions\"],\"pid\":7,\"message\":\"APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"alerts\",\"plugins\",\"alerting\"],\"pid\":7,\"message\":\"APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"info\",\"plugins\",\"monitoring\",\"monitoring\"],\"pid\":7,\"message\":\"config sourced from: production cluster\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"info\",\"savedobjects-service\"],\"pid\":7,\"message\":\"Waiting until all Elasticsearch nodes are compatible with Kibana before starting saved objects migrations...\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:28Z\",\"tags\":[\"info\",\"savedobjects-service\"],\"pid\":7,\"message\":\"Starting saved objects migrations\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:28Z\",\"tags\":[\"info\",\"plugins-system\"],\"pid\":7,\"message\":\"Starting [96] plugins: [taskManager,licensing,globalSearch,globalSearchProviders,code,usageCollection,xpackLegacy,telemetryCollectionManager,telemetry,telemetryCollectionXpack,kibanaUsageCollection,securityOss,newsfeed,mapsLegacy,kibanaLegacy,translations,share,legacyExport,embeddable,uiActionsEnhanced,expressions,data,home,observability,cloud,console,consoleExtensions,apmOss,searchprofiler,painlessLab,grokdebugger,management,indexPatternManagement,advancedSettings,fileUpload,savedObjects,dashboard,visualizations,visTypeTable,visTypeVega,visTypeTimelion,timelion,features,upgradeAssistant,security,snapshotRestore,enterpriseSearch,encryptedSavedObjects,ingestManager,indexManagement,remoteClusters,crossClusterReplication,indexLifecycleManagement,dashboardMode,beatsManagement,transform,ingestPipelines,maps,licenseManagement,graph,dataEnhanced,visTypeMarkdown,tileMap,regionMap,inputControlVis,visualize,esUiShared,charts,lens,visTypeVislib,visTypeTimeseries,rollup,visTypeTagcloud,visTypeMetric,watcher,discover,discoverEnhanced,savedObjectsManagement,spaces,reporting,lists,eventLog,actions,case,alerts,stackAlerts,triggersActionsUi,ml,securitySolution,infra,monitoring,logstash,apm,uptime,bfetch,canvas]\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:28Z\",\"tags\":[\"info\",\"plugins\",\"taskManager\",\"taskManager\"],\"pid\":7,\"message\":\"TaskManager is identified by the Kibana UUID: bf01f365-e094-4cde-940d-3e0db65fa22a\"}\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:28,778Z\", \"level\": \"INFO\", \"component\": \"o.e.c.m.MetadataIndexTemplateService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"adding template [.management-beats] for index patterns [.management-beats]\", \"cluster.uuid\": \"8uh2w1CUSGyWYR_OvaKx6g\", \"node.id\": \"e2ZKzgw4Q4aCf2w5ljWr1A\" }\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:28Z\",\"tags\":[\"info\",\"plugins\",\"crossClusterReplication\"],\"pid\":7,\"message\":\"Your basic license does not support crossClusterReplication. Please upgrade your license.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:28Z\",\"tags\":[\"info\",\"plugins\",\"watcher\"],\"pid\":7,\"message\":\"Your basic license does not support watcher. Please upgrade your license.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:28Z\",\"tags\":[\"info\",\"plugins\",\"monitoring\",\"monitoring\",\"kibana-monitoring\"],\"pid\":7,\"message\":\"Starting monitoring stats collection\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:29Z\",\"tags\":[\"error\",\"elasticsearch\",\"data\"],\"pid\":7,\"message\":\"[version_conflict_engine_exception]: [task:Lens-lens_telemetry]: version conflict, document already exists (current version [745])\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:29Z\",\"tags\":[\"error\",\"elasticsearch\",\"data\"],\"pid\":7,\"message\":\"[version_conflict_engine_exception]: [task:Actions-actions_telemetry]: version conflict, document already exists (current version [745])\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:29Z\",\"tags\":[\"error\",\"elasticsearch\",\"data\"],\"pid\":7,\"message\":\"[version_conflict_engine_exception]: [task:endpoint:user-artifact-packager:1.0.0]: version conflict, document already exists (current version [310913])\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:29Z\",\"tags\":[\"error\",\"elasticsearch\",\"data\"],\"pid\":7,\"message\":\"[version_conflict_engine_exception]: [task:Alerting-alerting_telemetry]: version conflict, document already exists (current version [745])\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:29Z\",\"tags\":[\"error\",\"elasticsearch\",\"data\"],\"pid\":7,\"message\":\"[version_conflict_engine_exception]: [task:apm-telemetry-task]: version conflict, document already exists (current version [1222])\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:29Z\",\"tags\":[\"listening\",\"info\"],\"pid\":7,\"message\":\"Server running at http://0:5601\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:31Z\",\"tags\":[\"info\",\"http\",\"server\",\"Kibana\"],\"pid\":7,\"message\":\"http server running at http://0:5601\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:32Z\",\"tags\":[\"warning\",\"plugins\",\"reporting\"],\"pid\":7,\"message\":\"Enabling the Chromium sandbox provides an additional layer of protection.\"}\n\n\nv View in Docker Desktop o View Config w Enable Watch","depth":4,"value":"0 social account(s) to be processed ...\ndocker_lamp_1 | \ndocker_lamp_1 | Done!\ndocker_lamp_1 | 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:bullhorn:ping --heartbeat > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-04-22 15:41:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:41:04 Running ['artisan' dialers:monitor-activities] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:41:06 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:41:07 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:41:08 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:41:10 Running ['artisan' crm:sync-hubspot-objects] [HubSpot] Syncing objects for Hubspot (abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4) since 2026-04-22 15:36:20 (delay: 0s)\ndocker_lamp_1 | Team TestV (2708d27a-7f31-4d90-bd2b-b5bde1c0211e) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team KioskAccount (dedab245-604c-4cd2-a83e-4b9e034b2772) is not yet assigned an owner. skipping...\ndocker_lamp_1 | [HubSpot] Syncing objects for InsightSquared (b2b115eb-93ce-4d1b-929c-173757df8fba) since 2026-02-17 15:09:59 (delay: 0s)\ndocker_lamp_1 | Team Aircall Demo (7980e5eb-b11c-4cee-9c21-8bb29ba85f3b) is not yet assigned an owner. skipping...\ndocker_lamp_1 | [HubSpot] Syncing objects for GoStudent UAT (b2d49a54-b645-4637-a7ae-a86cfce6e8e4) since 2026-02-17 15:09:59 (delay: 1s)\ndocker_lamp_1 | [HubSpot] Syncing objects for JustCall (c6b9d6b0-b48d-4832-a68c-a57d60651888) since 2026-02-17 15:07:41 (delay: 1s)\ndocker_lamp_1 | Team Twilio Video (c334ca55-b230-411c-b10e-31c8204bd07b) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team My Test Account 3000 (dbc9990d-b35f-4e38-9550-22cdd6059514) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team test (7997eb70-8aa4-491a-870d-311977568df4) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Test (5e06dcee-0613-470e-9a77-2c283198f3bf) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team test ogg auto sync (da44776e-306f-427a-83d8-a1b4baa5537e) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Tourlaner (d9b71080-388b-4cf5-8175-aa0f29bee635) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Dispatched 4 HubSpot sync jobs\ndocker_lamp_1 | 2026-04-22 15:41:11 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:sync-hubspot-objects > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | 2026-04-22 15:41:11 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ....... 44.08ms DONE\ndocker_lamp_1 | 2026-04-22 15:41:11 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | 2026-04-22 15:41:12 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ...... 577.01ms DONE\ndocker_lamp_1 | 2026-04-22 15:41:12 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | 2026-04-22 15:41:12 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ....... 16.42ms DONE\ndocker_lamp_1 | 2026-04-22 15:41:12 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | 2026-04-22 15:41:12 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ....... 17.23ms DONE\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-04-22 15:42:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:42:03 Running ['artisan' dialers:monitor-activities] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:42:05 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:42:06 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:42:07 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:42:09 Running ['artisan' conference:monitor:count] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:42:10 Running ['artisan' mailbox:batch:create] ....... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:create > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | 2026-04-22 15:42:14 Jiminny\\Jobs\\Mailbox\\CreateBatches ............. RUNNING\ndocker_lamp_1 | 2026-04-22 15:42:14 Jiminny\\Jobs\\Mailbox\\CreateBatches ........ 87.60ms DONE\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-04-22 15:43:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:43:04 Running ['artisan' dialers:monitor-activities] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:43:06 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:43:08 Running ['artisan' mailbox:skip-lists:refresh] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:43:10 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:43:11 Running ['artisan' mailbox:batch:retry-failed --max-batches=15] in background 5.15ms DONE\ndocker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' mailbox:batch:retry-failed --max-batches=15 > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish \"framework/schedule-390defd641effba0f73a895e426ded4cf2ba7f11\" \"$?\") > '/dev/null' 2>&1 & \ndocker_lamp_1 | 2026-04-22 15:43:11 Running ['artisan' calendar:sync --dateMode=daily] 2026-04-22 15:43:19 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents ....... RUNNING\ndocker_lamp_1 | 2026-04-22 15:43:20 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents ....... 1s DONE\ndocker_lamp_1 | 2026-04-22 15:43:22 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents ....... RUNNING\ndocker_lamp_1 | 10s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' calendar:sync --dateMode=daily > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | 2026-04-22 15:43:23 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents . 596.48ms DONE\ndocker_lamp_1 | 2026-04-22 15:43:23 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents ....... RUNNING\ndocker_lamp_1 | 2026-04-22 15:43:23 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents .. 52.11ms DONE\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-04-22 15:44:02 Running ['artisan' meeting-bot:schedule-bot] ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:44:04 Running ['artisan' dialers:monitor-activities] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:44:05 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:44:07 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:44:09 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:44:11 Running ['artisan' conference:monitor:count] ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:44:13 Running ['artisan' crm:sync-objects] Syncing objects for Salesforce Team (6473c918-d8db-4ded-a52b-4febfd7b7c02) since 2026-03-19 20:14:10 (delay: 0s)\ndocker_lamp_1 | Team Edge Communications (c3a725ef-fc5e-4c7b-9231-6379b2834101) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Burlington Textiles Corp of America (72c0ccae-28a5-44bd-a0b4-958e73135463) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Uber (1c523aed-80e3-4c44-b215-7f0b027a03a2) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Vonage (c6030540-121e-4d6b-9261-8d82105817bf) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Nexmo (eaba09e1-4973-4077-b041-ad389248318c) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team SF (018e2d7c-ed01-4af2-be33-5c28f454a185) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team NewAccount (27325244-25d7-4431-aac4-9ddb2bde6b45) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Syncing objects for Pipedrive, Inc. (51467630-d89d-480b-be20-933e64a042f7) since 2026-04-08 18:14:13 (delay: 2s)\ndocker_lamp_1 | Syncing objects for Copper (396ed57c-e3c4-49be-8290-37c32955f7c7) since 2026-04-22 15:14:16 (delay: 4s)\ndocker_lamp_1 | Syncing objects for Pipedrive External Test (fda3cbdf-1117-4ba5-86f8-775f548b3a28) since 2026-04-08 18:14:16 (delay: 6s)\ndocker_lamp_1 | Syncing objects for Close (3ff5a02a-86fb-4357-b1d6-a04e26c38602) since 2026-04-22 15:14:20 (delay: 8s)\ndocker_lamp_1 | Team DeewanyAccount (cfaf90cb-ecf0-432c-a655-a22d9985e4c8) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Syncing objects for Bullhorn (1640a0ac-19da-4c3b-90f7-87525f07a6d2) since 2026-02-19 13:28:05 (delay: 10s)\ndocker_lamp_1 | Team Horen test (93d8f8f2-dd61-4137-97de-a02a9d3751bb) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Ahmed Hamadi (191379e4-5e90-4419-8ffd-4804a5abb565) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Ahmed Testing (3b2e1fad-bc5f-4322-84d2-a7440f78c5d7) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Ahmed jiminny (1dbd47b8-bbf8-4bbb-8e28-67b9c91f566e) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Testers Inc (ec0767d5-1248-4447-b316-94dd9a30d52b) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Copper Team (817c8ce1-8e22-4ac8-8fca-8df1131963ab) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team TheBestPlace Ever (dbb998f9-70eb-4f11-a9f2-d9f6157f4035) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Horen's apartments (e10bb4ea-6c89-4fe3-be3b-0a278aa81180) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team New Org Test (2cb527b6-73a4-46be-b7b2-b0097b494cc5) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Pipedrive test org (18ea22be-6e0b-461c-a4db-536c022aac73) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team BigChairs Inc. (16dcfaf9-44b4-4cc2-9ead-f98dd0cad4dd) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Syncing objects for GL 500 (0c33bf2d-1c77-4200-8ed6-6147ad444c30) since 2026-02-19 13:26:04 (delay: 12s)\ndocker_lamp_1 | Team Loren X (243d7d4b-1849-46c2-9dfb-4462f52020ef) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Laravel Company 2024 (e466d657-c26d-4281-9ed5-73890fe711f6) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Barbara Fuchs (bea0fcf3-f8f9-471d-92c4-d1b7d1a2e228) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Test salesforce auto sync (2c2e652e-1cbb-4620-852d-e73bef0ab4a9) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Bullhorn (83c14120-cdb1-42e9-b0ac-16ef11320a8f) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Globo Tech (fb8d5211-3c4d-4804-9223-b091670ffd79) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Laravel 11 Company 2024 (780c6836-764b-4e47-9a05-cfefbab6e590) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Nikon7 (e9bff902-edc3-4b0b-89b1-2a9aa43d940a) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Syncing objects for Dev Zoho CRM client (1ece66c8-feb1-4df1-b321-21607daf4623) since 2026-04-22 15:14:26 (delay: 14s)\ndocker_lamp_1 | ........... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:sync-objects > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | 2026-04-22 15:44:15 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:15 Jiminny\\Jobs\\Crm\\SyncObjects .............. 49.60ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:15 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:16 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob 601.82ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:17 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:17 Jiminny\\Jobs\\Crm\\SyncObjects .............. 24.34ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:19 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:20 Jiminny\\Jobs\\Crm\\SyncObjects ................... 1s DONE\ndocker_lamp_1 | 2026-04-22 15:44:20 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:21 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob 557.11ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:21 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:21 Jiminny\\Jobs\\Crm\\SyncObjects .............. 58.10ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:23 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:24 Jiminny\\Jobs\\Crm\\SyncObjects ................... 1s DONE\ndocker_lamp_1 | 2026-04-22 15:44:25 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:26 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob 787.71ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:26 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:26 Jiminny\\Jobs\\Crm\\SyncObjects .............. 18.07ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:27 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:27 Jiminny\\Jobs\\Crm\\SyncObjects .............. 19.18ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:29 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:33 Jiminny\\Jobs\\Crm\\SyncObjects ................... 3s DONE\ndocker_lamp_1 | 2026-04-22 15:44:33 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:34 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob 848.26ms DONE\ndocker_lamp_1 | 2026-04-22 15:44:35 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob RUNNING\ndocker_lamp_1 | 2026-04-22 15:44:35 Jiminny\\Jobs\\Crm\\Salesforce\\FetchSalesforceEntitiesJob 431.28ms DONE\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-04-22 15:45:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:03 Running ['artisan' dialers:monitor-activities] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:06 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:09 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:10 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:12 Running ['artisan' activity:purge-stale] ....... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:purge-stale > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:15 Running ['artisan' mailbox:text-relay:sync] {\ndocker_lamp_1 | \"error\": \"invalid_request\",\ndocker_lamp_1 | \"error_description\": \"Invalid impersonation \\u0026quot;sub\\u0026quot; field: @\"\ndocker_lamp_1 | }\ndocker_lamp_1 | .... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:text-relay:sync > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:17 Running ['artisan' conference:pre-meeting-notification] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:pre-meeting-notification > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-04-22 15:45:19 Running ['artisan' conference:monitor:start] ... 4s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:start > '/proc/1/fd/1' 2>&1 \nunexpected EOF\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/infrastructure/dev/docker (develop) $ work\nWARN[0000] /Users/lukas/jiminny/infrastructure/dev/docker/docker-compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion \nAttaching to blackfire-1, datadog-1, jiminny_ext-1, mariadb-1, docker_lamp_1, elasticsearch, kibana, ngrok, redis\nredis | 1:C 23 Apr 2026 07:55:51.731 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo\nredis | 1:C 23 Apr 2026 07:55:51.731 # Redis version=5.0.14, bits=64, commit=00000000, modified=0, pid=1, just started\nredis | 1:C 23 Apr 2026 07:55:51.731 # Configuration loaded\nmariadb-1 | 2026-04-23 07:55:51+00:00 [Note] [Entrypoint]: Entrypoint script for MariaDB Server 1:11.4.5+maria~ubu2404 started.\nredis | 1:M 23 Apr 2026 07:55:51.741 * Running mode=standalone, port=6379.\nredis | 1:M 23 Apr 2026 07:55:51.741 # Server initialized\nredis | 1:M 23 Apr 2026 07:55:51.741 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled.\nredis | 1:M 23 Apr 2026 07:55:51.744 * Reading RDB preamble from AOF file...\nredis | 1:M 23 Apr 2026 07:55:51.745 * Reading the remaining AOF tail...\nblackfire-1 | [2026-04-23T07:55:51Z] ERROR: The server ID parameter is not set. Please run 'blackfire-agent -register' to configure it.\nblackfire-1 | usage blackfire-agent [options]\nblackfire-1 | --collector=\"https://blackfire.io\": Sets the URL of Blackfire's data collector\nblackfire-1 | --config=\"/etc/blackfire/agent\": Sets the path to the configuration file\nblackfire-1 | -d: Prints the current configuration\nblackfire-1 | --http-proxy=\"\": Sets the HTTP proxy to use\nblackfire-1 | --https-proxy=\"\": Sets the HTTPS proxy to use\nblackfire-1 | --log-file=\"stderr\": Sets the path of the log file. Use stderr to log to stderr\nblackfire-1 | --log-level=\"1\": log verbosity level (4: debug, 3: info, 2: warning, 1: error)\nblackfire-1 | --register: Helps you with registering the agent\nblackfire-1 | --server-id=\"\": Sets the server id used to authenticate with Blackfire API\nblackfire-1 | --server-token=\"\": Sets the server token used to authenticate with Blackfire API. It is unsafe to set this from the command line\nblackfire-1 | --socket=\"unix:///var/run/blackfire/agent.sock\": Sets the socket the agent should read traces from. Possible value can be a unix socket or a TCP address. ie: unix:///var/run/blackfire/agent.sock or tcp://127.0.0.1:8307\nblackfire-1 | --test: Tests the configuration\nblackfire-1 | --timeout=\"15s\": Sets the Blackfire connection timeout\nblackfire-1 | -v: Prints the version number\nngrok | t=2026-04-23T07:55:52+0000 lvl=info msg=\"no configuration paths supplied\"\nngrok | t=2026-04-23T07:55:52+0000 lvl=info msg=\"using configuration at default config path\" path=/home/ngrok/.ngrok2/ngrok.yml\nngrok | t=2026-04-23T07:55:52+0000 lvl=info msg=\"open config file\" path=/home/ngrok/.ngrok2/ngrok.yml err=nil\nblackfire-1 exited with code 1\njiminny_ext-1 exited with code 0\nngrok | t=2026-04-23T07:55:52+0000 lvl=info msg=\"starting web service\" obj=web addr=0.0.0.0:4040\ndocker_lamp_1 | + main\ndocker_lamp_1 | + declare START_DIR\ndocker_lamp_1 | +++ realpath /scripts/init-dev\ndocker_lamp_1 | ++ dirname /scripts/init-dev\ndocker_lamp_1 | + START_DIR=/scripts\ndocker_lamp_1 | + readonly START_DIR\ndocker_lamp_1 | + source /scripts/storage_init.sh\ndocker_lamp_1 | ++ set -o errexit\ndocker_lamp_1 | ++ set -o nounset\ndocker_lamp_1 | ++ set -o pipefail\ndocker_lamp_1 | + create_bind_mount\ndocker_lamp_1 | + [[ 0 == \\1 ]]\ndocker_lamp_1 | + configure_xdebug\ndocker_lamp_1 | + j2 /root/.j2_templates/xdebug/xdebug.ini.j2\nmariadb-1 | 2026-04-23 07:55:52+00:00 [Warn] [Entrypoint]: /sys/fs/cgroup///memory.pressure not writable, functionality unavailable to MariaDB\nmariadb-1 | 2026-04-23 07:55:52+00:00 [Note] [Entrypoint]: Switching to dedicated user 'mysql'\nmariadb-1 | 2026-04-23 07:55:52+00:00 [Note] [Entrypoint]: Entrypoint script for MariaDB Server 1:11.4.5+maria~ubu2404 started.\ndatadog-1 | [s6-init] making user provided files available at /var/run/s6/etc...exited 0.\nngrok | t=2026-04-23T07:55:52+0000 lvl=info msg=\"tunnel session started\" obj=tunnels.session\nngrok | t=2026-04-23T07:55:52+0000 lvl=info msg=\"client session established\" obj=csess id=1c8f9eb905ac\nmariadb-1 | 2026-04-23 07:55:53+00:00 [Note] [Entrypoint]: MariaDB upgrade information missing, assuming required\nmariadb-1 | 2026-04-23 07:55:53+00:00 [Note] [Entrypoint]: MariaDB upgrade (mariadb-upgrade or creating healthcheck users) required, but skipped due to $MARIADB_AUTO_UPGRADE setting\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] Starting MariaDB 11.4.5-MariaDB-ubu2404 source revision 0771110266ff5c04216af4bf1243c65f8c67ccf4 server_uid zs02SyAxkg7epm72pZgde8TQHsk= as process 1\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Compressed tables use zlib 1.3\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Number of transaction pools: 1\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Using ARMv8 crc32 + pmull instructions\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Using liburing\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Initializing buffer pool, total size = 128.000MiB, chunk size = 2.000MiB\nngrok | t=2026-04-23T07:55:53+0000 lvl=info msg=\"started tunnel\" obj=tunnels name=\"command_line (http)\" addr=http://lamp:3080 url=http://lukask.ngrok.io\nngrok | t=2026-04-23T07:55:53+0000 lvl=info msg=\"started tunnel\" obj=tunnels name=command_line addr=http://lamp:3080 url=https://lukask.ngrok.io\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Completed initialization of buffer pool\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: File system buffers for log disabled (block size=512 bytes)\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: End of log at LSN=7618260638\ndocker_lamp_1 | + configure_blackfire\ndocker_lamp_1 | + j2 /root/.j2_templates/blackfire/extension.ini.j2\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Opened 3 undo tablespaces\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: 128 rollback segments in 3 undo tablespaces are active.\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Setting file './ibtmp1' size to 12.000MiB. Physically writing the file full; Please wait ...\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: File './ibtmp1' size is now 12.000MiB.\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: log sequence number 7618260638; transaction id 11490938\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] Plugin 'FEEDBACK' is disabled.\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] Plugin 'wsrep-provider' is disabled.\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Loading buffer pool(s) from /var/lib/mysql/ib_buffer_pool\ndocker_lamp_1 | + j2 /root/.j2_templates/blackfire/cli.ini.j2\nmariadb-1 | 2026-04-23 7:55:53 0 [Note] InnoDB: Buffer pool(s) load completed at 260423 7:55:53\nngrok | t=2026-04-23T07:55:53+0000 lvl=info msg=\"update available\" obj=updater\ndocker_lamp_1 | + declare EMPTY_DB\ndocker_lamp_1 | + db_is_empty\ndocker_lamp_1 | ++ find /var/lib/mysql/ -maxdepth 1\ndocker_lamp_1 | ++ wc -l\ndocker_lamp_1 | + [[ 11 -lt 5 ]]\ndocker_lamp_1 | + EMPTY_DB=0\ndocker_lamp_1 | + readonly EMPTY_DB\ndocker_lamp_1 | + [[ 0 -eq 1 ]]\ndocker_lamp_1 | + [[ local == \\l\\o\\c\\a\\l ]]\ndocker_lamp_1 | + set_nginx_domain dev.jiminny.com\ndocker_lamp_1 | + declare -r DOMAIN_NAME=dev.jiminny.com\ndocker_lamp_1 | + cp -f /etc/nginx/nginx_template.conf /etc/nginx/nginx.conf\ndocker_lamp_1 | + sed -i -E s~_JIMINNY_DOMAIN~app.dev.jiminny.com~g /etc/nginx/nginx.conf\ndocker_lamp_1 | + sed -i -E s~_JIMINNY_EXT_DOMAIN~ext.dev.jiminny.com~g /etc/nginx/nginx.conf\ndocker_lamp_1 | + sed -i -E s~_JIMINNY_WEB_DOMAIN~www.dev.jiminny.com~g /etc/nginx/nginx.conf\ndocker_lamp_1 | + [[ -n 3399 ]]\ndocker_lamp_1 | + sed -i -E 's~http:\\/\\/jiminny_ext:8080~http://jiminny_ext:3399~g' /etc/nginx/nginx.conf\ndocker_lamp_1 | + [[ -n host.docker.internal ]]\ndocker_lamp_1 | + sed -i -E 's~http:\\/\\/jiminny_ext~http://host.docker.internal~g' /etc/nginx/nginx.conf\ndocker_lamp_1 | + build_dev\ndocker_lamp_1 | + cd /home/jiminny/\ndocker_lamp_1 | + create_dot_env_local_file\ndocker_lamp_1 | + cp -f /home/jiminny/.env.local /home/jiminny/.env.local.bak\ndocker_lamp_1 | + create_dot_env\ndocker_lamp_1 | + [[ -f /home/jiminny/.env ]]\ndocker_lamp_1 | + return\ndocker_lamp_1 | + declare DB_ADMIN_PASSWORD\ndocker_lamp_1 | + declare DB_ADMIN_USERNAME\ndocker_lamp_1 | + declare DB_DEV_PASSWORD\ndocker_lamp_1 | + declare DB_DEV_USERNAME\ndocker_lamp_1 | + declare DB_ROOT_PASSWORD\ndocker_lamp_1 | + declare DB_ROOT_USERNAME\ndocker_lamp_1 | + declare DB_WEB_PASSWORD\ndocker_lamp_1 | + declare DB_WEB_USERNAME\ndocker_lamp_1 | ++ jq -r .DB_ADMIN_PASSWORD /home/jiminny/dev.json\ndatadog-1 | [s6-init] ensuring user provided files have correct perms...exited 0.\ndocker_lamp_1 | + DB_ADMIN_PASSWORD='dgyt$rTe21-d'\ndocker_lamp_1 | ++ jq -r .DB_ADMIN_USERNAME /home/jiminny/dev.json\ndatadog-1 | [fix-attrs.d] applying ownership & permissions fixes...\ndocker_lamp_1 | + DB_ADMIN_USERNAME=jmnyadmin\ndocker_lamp_1 | ++ jq -r .DB_DEV_PASSWORD /home/jiminny/dev.json\ndatadog-1 | [fix-attrs.d] done.\ndocker_lamp_1 | + DB_DEV_PASSWORD=rTr4sdQA65-Ad\n\n\ndatadog-1 | [cont-init.d] executing container initialization scripts...\ndocker_lamp_1 | ++ jq -r .DB_DEV_USERNAME /home/jiminny/dev.json\ndocker_lamp_1 | + DB_DEV_USERNAME=jmnydev\ndocker_lamp_1 | ++ jq -r .DB_ROOT_PASSWORD /home/jiminny/dev.json\ndatadog-1 | [cont-init.d] 01-check-apikey.sh: executing... \ndocker_lamp_1 | + DB_ROOT_PASSWORD=b7h5-1fH3e54J\ndocker_lamp_1 | ++ jq -r .DB_ROOT_USERNAME /home/jiminny/dev.json\ndocker_lamp_1 | + DB_ROOT_USERNAME=root\ndocker_lamp_1 | ++ jq -r .DB_WEB_PASSWORD /home/jiminny/dev.json\ndatadog-1 | \ndatadog-1 | ==================================================================================\ndatadog-1 | You must set an DD_API_KEY environment variable to run the Datadog Agent container\ndatadog-1 | ==================================================================================\ndatadog-1 | \ndatadog-1 | [cont-init.d] 01-check-apikey.sh: exited 1.\ndocker_lamp_1 | + DB_WEB_PASSWORD=aR5-EWf23b8da\ndocker_lamp_1 | ++ jq -r .DB_WEB_USERNAME /home/jiminny/dev.json\ndocker_lamp_1 | + DB_WEB_USERNAME=jmnyweb\ndocker_lamp_1 | + readonly DB_ADMIN_PASSWORD\ndocker_lamp_1 | + readonly DB_ADMIN_USERNAME\ndocker_lamp_1 | + readonly DB_DEV_PASSWORD\ndocker_lamp_1 | + readonly DB_DEV_USERNAME\ndocker_lamp_1 | + readonly DB_ROOT_PASSWORD\ndocker_lamp_1 | + readonly DB_ROOT_USERNAME\ndocker_lamp_1 | + readonly DB_WEB_PASSWORD\ndocker_lamp_1 | + readonly DB_WEB_USERNAME\ndocker_lamp_1 | + sed -i -E 's~DB_USERNAME=.*$~DB_USERNAME=jmnyadmin~g' /home/jiminny/.env\ndocker_lamp_1 | + sed -i -E 's~DB_PASSWORD=.*$~DB_PASSWORD=dgyt$rTe21-d~g' /home/jiminny/.env\ndocker_lamp_1 | + sed -i -E 's~DB_HOST=.*$~DB_HOST=mariadb~g' /home/jiminny/.env\ndocker_lamp_1 | + cp -f /home/jiminny/.env /home/jiminny/.env.root\ndocker_lamp_1 | + cp -f /home/jiminny/.env /home/jiminny/.env.migrate\ndocker_lamp_1 | + sed -i -E 's~DB_PASSWORD=.*$~DB_PASSWORD=b7h5-1fH3e54J~g' /home/jiminny/.env.migrate\ndocker_lamp_1 | + sed -i -E 's~DB_PASSWORD=.*$~DB_PASSWORD=b7h5-1fH3e54J~g' /home/jiminny/.env.root\ndocker_lamp_1 | + sed -i -E 's~DB_USERNAME=.*$~DB_USERNAME=root~g' /home/jiminny/.env.migrate\ndocker_lamp_1 | + sed -i -E 's~DB_USERNAME=.*$~DB_USERNAME=root~g' /home/jiminny/.env.root\ndocker_lamp_1 | + cp -f /home/jiminny/.env /home/jiminny/.env.local\ndocker_lamp_1 | + echo ''\ndocker_lamp_1 | + echo 'DB_ADMIN_PASSWORD=dgyt$rTe21-d'\ndocker_lamp_1 | + echo DB_ADMIN_USERNAME=jmnyadmin\ndocker_lamp_1 | + echo DB_DEV_PASSWORD=rTr4sdQA65-Ad\ndocker_lamp_1 | + echo DB_DEV_USERNAME=jmnydev\ndocker_lamp_1 | + echo DB_ROOT_PASSWORD=b7h5-1fH3e54J\ndocker_lamp_1 | + echo DB_ROOT_USERNAME=root\ndocker_lamp_1 | + echo DB_WEB_PASSWORD=aR5-EWf23b8da\ndocker_lamp_1 | + echo DB_WEB_USERNAME=jmnyweb\ndocker_lamp_1 | + [[ false == \\f\\a\\l\\s\\e ]]\ndocker_lamp_1 | + declare COMPOSER_PARAM=--prefer-dist\ndocker_lamp_1 | + [[ 0 -eq 1 ]]\ndocker_lamp_1 | + composer install --prefer-dist\ndatadog-1 exited with code 1\nmariadb-1 | 2026-04-23 7:55:54 0 [Note] Server socket created on IP: '0.0.0.0'.\nmariadb-1 | 2026-04-23 7:55:54 0 [Note] Server socket created on IP: '::'.\nmariadb-1 | 2026-04-23 7:55:54 0 [Note] mariadbd: Event Scheduler: Loaded 0 events\nmariadb-1 | 2026-04-23 7:55:54 0 [Note] mariadbd: ready for connections.\nmariadb-1 | Version: '11.4.5-MariaDB-ubu2404' socket: '/run/mysqld/mysqld.sock' port: 3306 mariadb.org binary distribution\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:56,514Z\", \"level\": \"INFO\", \"component\": \"o.e.n.Node\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"version[7.10.2], pid[7], build[default/docker/747e1cc71def077253878a59143c1f785afa92b9/2021-01-13T04:42:47.157277Z], OS[Linux/6.12.54-linuxkit/aarch64], JVM[AdoptOpenJDK/OpenJDK 64-Bit Server VM/15.0.1/15.0.1+9]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:56,517Z\", \"level\": \"INFO\", \"component\": \"o.e.n.Node\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"JVM home [/usr/share/elasticsearch/jdk], using bundled JDK [true]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:56,518Z\", \"level\": \"INFO\", \"component\": \"o.e.n.Node\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"JVM arguments [-Xshare:auto, -Des.networkaddress.cache.ttl=60, -Des.networkaddress.cache.negative.ttl=10, -XX:+AlwaysPreTouch, -Xss1m, -Djava.awt.headless=true, -Dfile.encoding=UTF-8, -Djna.nosys=true, -XX:-OmitStackTraceInFastThrow, -XX:+ShowCodeDetailsInExceptionMessages, -Dio.netty.noUnsafe=true, -Dio.netty.noKeySetOptimization=true, -Dio.netty.recycler.maxCapacityPerThread=0, -Dio.netty.allocator.numDirectArenas=0, -Dlog4j.shutdownHookEnabled=false, -Dlog4j2.disable.jmx=true, -Djava.locale.providers=SPI,COMPAT, -Xms1g, -Xmx1g, -XX:+UseG1GC, -XX:G1ReservePercent=25, -XX:InitiatingHeapOccupancyPercent=30, -Djava.io.tmpdir=/tmp/elasticsearch-10124985765930456581, -XX:+HeapDumpOnOutOfMemoryError, -XX:HeapDumpPath=data, -XX:ErrorFile=logs/hs_err_pid%p.log, -Xlog:gc*,gc+age=trace,safepoint:file=logs/gc.log:utctime,pid,tags:filecount=32,filesize=64m, -Des.cgroups.hierarchy.override=/, -Xms700m, -Xmx700m, -XX:MaxDirectMemorySize=367001600, -Des.path.home=/usr/share/elasticsearch, -Des.path.conf=/usr/share/elasticsearch/config, -Des.distribution.flavor=default, -Des.distribution.type=docker, -Des.bundled_jdk=true]\" }\ndocker_lamp_1 | Installing dependencies from lock file (including require-dev)\ndocker_lamp_1 | Verifying lock file contents can be installed on current platform.\ndocker_lamp_1 | Nothing to install, update or remove\ndocker_lamp_1 | Package doctrine/annotations is abandoned, you should avoid using it. No replacement was suggested.\ndocker_lamp_1 | Package symfony/debug is abandoned, you should avoid using it. Use symfony/error-handler instead.\ndocker_lamp_1 | Generating optimized autoload files\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,062Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [aggs-matrix-stats]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,062Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [analysis-common]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,063Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [constant-keyword]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,063Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [flattened]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,063Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [frozen-indices]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,063Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [ingest-common]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,063Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [ingest-geoip]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,063Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [ingest-user-agent]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,063Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [kibana]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,064Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [lang-expression]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,064Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [lang-mustache]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [lang-painless]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [mapper-extras]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [mapper-version]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [parent-join]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [percolator]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [rank-eval]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [reindex]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [repositories-metering-api]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [repository-url]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [search-business-rules]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,065Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [searchable-snapshots]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [spatial]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [transform]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [transport-netty4]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [unsigned-long]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [vectors]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [wildcard]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-analytics]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-async]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-async-search]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-autoscaling]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-ccr]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-core]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,066Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-data-streams]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-deprecation]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-enrich]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-eql]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-graph]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-identity-provider]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-ilm]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-logstash]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-ml]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-monitoring]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-ql]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-rollup]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-security]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,067Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-sql]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,068Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-stack]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,068Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-voting-only-node]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,068Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"loaded module [x-pack-watcher]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,068Z\", \"level\": \"INFO\", \"component\": \"o.e.p.PluginsService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"no plugins loaded\" }\nelasticsearch | {\"type\": \"deprecation\", \"timestamp\": \"2026-04-23T07:55:59,122Z\", \"level\": \"DEPRECATION\", \"component\": \"o.e.d.c.s.Settings\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"[node.data] setting was deprecated in Elasticsearch and will be removed in a future release! See the breaking changes documentation for the next major version.\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,139Z\", \"level\": \"INFO\", \"component\": \"o.e.e.NodeEnvironment\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"using [1] data paths, mounts [[/usr/share/elasticsearch/data (/dev/vda1)]], net usable_space [14.5gb], net total_space [58.3gb], types [ext4]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,139Z\", \"level\": \"INFO\", \"component\": \"o.e.e.NodeEnvironment\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"heap size [700mb], compressed ordinary object pointers [true]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:55:59,263Z\", \"level\": \"INFO\", \"component\": \"o.e.n.Node\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"node name [e802ad473a4f], node ID [e2ZKzgw4Q4aCf2w5ljWr1A], cluster name [docker-cluster], roles [transform, master, remote_cluster_client, data, ml, data_content, data_hot, data_warm, data_cold, ingest]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:06,887Z\", \"level\": \"INFO\", \"component\": \"o.e.x.m.p.l.CppLogMessageHandler\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"[controller/214] [Main.cc@114] controller (64 bit): Version 7.10.2 (Build 40a3af639d4698) Copyright (c) 2020 Elasticsearch BV\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:08,335Z\", \"level\": \"INFO\", \"component\": \"o.e.t.NettyAllocator\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"creating NettyAllocator with the following configs: [name=unpooled, suggested_max_allocation_size=256kb, factors={es.unsafe.use_unpooled_allocator=null, g1gc_enabled=true, g1gc_region_size=1mb, heap_size=700mb}]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:08,444Z\", \"level\": \"INFO\", \"component\": \"o.e.d.DiscoveryModule\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"using discovery type [single-node] and seed hosts providers [settings]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:08,962Z\", \"level\": \"WARN\", \"component\": \"o.e.g.DanglingIndicesState\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"gateway.auto_import_dangling_indices is disabled, dangling indices will not be automatically detected or imported and must be managed manually\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:09,262Z\", \"level\": \"INFO\", \"component\": \"o.e.n.Node\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"initialized\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:09,263Z\", \"level\": \"INFO\", \"component\": \"o.e.n.Node\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"starting ...\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:09,430Z\", \"level\": \"INFO\", \"component\": \"o.e.t.TransportService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"publish_address {172.18.0.2:9300}, bound_addresses {[::]:9300}\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:09,769Z\", \"level\": \"INFO\", \"component\": \"o.e.c.c.Coordinator\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"cluster UUID [8uh2w1CUSGyWYR_OvaKx6g]\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:09,886Z\", \"level\": \"INFO\", \"component\": \"o.e.c.s.MasterService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"elected-as-master ([1] nodes joined)[{e802ad473a4f}{e2ZKzgw4Q4aCf2w5ljWr1A}{cv7PSrNNRgOBYzeY8N23MA}{172.18.0.2}{172.18.0.2:9300}{cdhilmrstw}{ml.machine_memory=4109217792, xpack.installed=true, transform.node=true, ml.max_open_jobs=20} elect leader, _BECOME_MASTER_TASK_, _FINISH_ELECTION_], term: 229, version: 8527, delta: master node changed {previous [], current [{e802ad473a4f}{e2ZKzgw4Q4aCf2w5ljWr1A}{cv7PSrNNRgOBYzeY8N23MA}{172.18.0.2}{172.18.0.2:9300}{cdhilmrstw}{ml.machine_memory=4109217792, xpack.installed=true, transform.node=true, ml.max_open_jobs=20}]}\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:09,973Z\", \"level\": \"INFO\", \"component\": \"o.e.c.s.ClusterApplierService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"master node changed {previous [], current [{e802ad473a4f}{e2ZKzgw4Q4aCf2w5ljWr1A}{cv7PSrNNRgOBYzeY8N23MA}{172.18.0.2}{172.18.0.2:9300}{cdhilmrstw}{ml.machine_memory=4109217792, xpack.installed=true, transform.node=true, ml.max_open_jobs=20}]}, term: 229, version: 8527, reason: Publication{term=229, version=8527}\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:10,019Z\", \"level\": \"INFO\", \"component\": \"o.e.h.AbstractHttpServerTransport\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"publish_address {172.18.0.2:9200}, bound_addresses {[::]:9200}\", \"cluster.uuid\": \"8uh2w1CUSGyWYR_OvaKx6g\", \"node.id\": \"e2ZKzgw4Q4aCf2w5ljWr1A\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:10,020Z\", \"level\": \"INFO\", \"component\": \"o.e.n.Node\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"started\", \"cluster.uuid\": \"8uh2w1CUSGyWYR_OvaKx6g\", \"node.id\": \"e2ZKzgw4Q4aCf2w5ljWr1A\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:10,388Z\", \"level\": \"INFO\", \"component\": \"o.e.l.LicenseService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"license [85e882e5-5714-4173-a5dd-9baa841494a0] mode [basic] - valid\", \"cluster.uuid\": \"8uh2w1CUSGyWYR_OvaKx6g\", \"node.id\": \"e2ZKzgw4Q4aCf2w5ljWr1A\" }\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:10,396Z\", \"level\": \"INFO\", \"component\": \"o.e.g.GatewayService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"recovered [15] indices into cluster_state\", \"cluster.uuid\": \"8uh2w1CUSGyWYR_OvaKx6g\", \"node.id\": \"e2ZKzgw4Q4aCf2w5ljWr1A\" }\nredis | 1:M 23 Apr 2026 07:56:12.207 * DB loaded from append only file: 20.465 seconds\nredis | 1:M 23 Apr 2026 07:56:12.208 * Ready to accept connections\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:12,454Z\", \"level\": \"INFO\", \"component\": \"o.e.c.r.a.AllocationService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"Cluster health status changed from [RED] to [YELLOW] (reason: [shards started [[activities][2]]]).\", \"cluster.uuid\": \"8uh2w1CUSGyWYR_OvaKx6g\", \"node.id\": \"e2ZKzgw4Q4aCf2w5ljWr1A\" }\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:26Z\",\"tags\":[\"info\",\"plugins-service\"],\"pid\":7,\"message\":\"Plugin \\\"visTypeXy\\\" is disabled.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:26Z\",\"tags\":[\"info\",\"plugins-service\"],\"pid\":7,\"message\":\"Plugin \\\"auditTrail\\\" is disabled.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:26Z\",\"tags\":[\"warning\",\"config\",\"deprecation\"],\"pid\":7,\"message\":\"Config key [monitoring.cluster_alerts.email_notifications.email_address] will be required for email notifications to work in 8.0.\\\"\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:26Z\",\"tags\":[\"info\",\"plugins-system\"],\"pid\":7,\"message\":\"Setting up [96] plugins: [taskManager,licensing,globalSearch,globalSearchProviders,code,usageCollection,xpackLegacy,telemetryCollectionManager,telemetry,telemetryCollectionXpack,kibanaUsageCollection,securityOss,newsfeed,mapsLegacy,kibanaLegacy,translations,share,legacyExport,embeddable,uiActionsEnhanced,expressions,data,home,observability,cloud,console,consoleExtensions,apmOss,searchprofiler,painlessLab,grokdebugger,management,indexPatternManagement,advancedSettings,fileUpload,savedObjects,dashboard,visualizations,visTypeTable,visTypeVega,visTypeTimelion,timelion,features,upgradeAssistant,security,snapshotRestore,enterpriseSearch,encryptedSavedObjects,ingestManager,indexManagement,remoteClusters,crossClusterReplication,indexLifecycleManagement,dashboardMode,beatsManagement,transform,ingestPipelines,maps,licenseManagement,graph,dataEnhanced,visTypeMarkdown,tileMap,regionMap,inputControlVis,visualize,esUiShared,charts,lens,visTypeVislib,visTypeTimeseries,rollup,visTypeTagcloud,visTypeMetric,watcher,discover,discoverEnhanced,savedObjectsManagement,spaces,reporting,lists,eventLog,actions,case,alerts,stackAlerts,triggersActionsUi,ml,securitySolution,infra,monitoring,logstash,apm,uptime,bfetch,canvas]\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"security\",\"config\"],\"pid\":7,\"message\":\"Generating a random key for xpack.security.encryptionKey. To prevent sessions from being invalidated on restart, please set xpack.security.encryptionKey in kibana.yml\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"security\",\"config\"],\"pid\":7,\"message\":\"Session cookies will be transmitted over insecure connections. This is not recommended.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"encryptedSavedObjects\",\"config\"],\"pid\":7,\"message\":\"Generating a random key for xpack.encryptedSavedObjects.encryptionKey. To be able to decrypt encrypted saved objects attributes after restart, please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"ingestManager\"],\"pid\":7,\"message\":\"Fleet APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"reporting\",\"config\"],\"pid\":7,\"message\":\"Generating a random key for xpack.reporting.encryptionKey. To prevent sessions from being invalidated on restart, please set xpack.reporting.encryptionKey in kibana.yml\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"reporting\",\"config\"],\"pid\":7,\"message\":\"Found 'server.host: \\\"0\\\"' in Kibana configuration. This is incompatible with Reporting. To enable Reporting to work, 'xpack.reporting.kibanaServer.hostname: 0.0.0.0' is being automatically to the configuration. You can change the setting to 'server.host: 0.0.0.0' or add 'xpack.reporting.kibanaServer.hostname: 0.0.0.0' in kibana.yml to prevent this message.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"reporting\",\"config\"],\"pid\":7,\"message\":\"Chromium sandbox provides an additional layer of protection, but is not supported for Linux CentOS 8.3.2011\\n OS. Automatically setting 'xpack.reporting.capture.browser.chromium.disableSandbox: true'.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"actions\",\"actions\"],\"pid\":7,\"message\":\"APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"warning\",\"plugins\",\"alerts\",\"plugins\",\"alerting\"],\"pid\":7,\"message\":\"APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"info\",\"plugins\",\"monitoring\",\"monitoring\"],\"pid\":7,\"message\":\"config sourced from: production cluster\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:27Z\",\"tags\":[\"info\",\"savedobjects-service\"],\"pid\":7,\"message\":\"Waiting until all Elasticsearch nodes are compatible with Kibana before starting saved objects migrations...\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:28Z\",\"tags\":[\"info\",\"savedobjects-service\"],\"pid\":7,\"message\":\"Starting saved objects migrations\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:28Z\",\"tags\":[\"info\",\"plugins-system\"],\"pid\":7,\"message\":\"Starting [96] plugins: [taskManager,licensing,globalSearch,globalSearchProviders,code,usageCollection,xpackLegacy,telemetryCollectionManager,telemetry,telemetryCollectionXpack,kibanaUsageCollection,securityOss,newsfeed,mapsLegacy,kibanaLegacy,translations,share,legacyExport,embeddable,uiActionsEnhanced,expressions,data,home,observability,cloud,console,consoleExtensions,apmOss,searchprofiler,painlessLab,grokdebugger,management,indexPatternManagement,advancedSettings,fileUpload,savedObjects,dashboard,visualizations,visTypeTable,visTypeVega,visTypeTimelion,timelion,features,upgradeAssistant,security,snapshotRestore,enterpriseSearch,encryptedSavedObjects,ingestManager,indexManagement,remoteClusters,crossClusterReplication,indexLifecycleManagement,dashboardMode,beatsManagement,transform,ingestPipelines,maps,licenseManagement,graph,dataEnhanced,visTypeMarkdown,tileMap,regionMap,inputControlVis,visualize,esUiShared,charts,lens,visTypeVislib,visTypeTimeseries,rollup,visTypeTagcloud,visTypeMetric,watcher,discover,discoverEnhanced,savedObjectsManagement,spaces,reporting,lists,eventLog,actions,case,alerts,stackAlerts,triggersActionsUi,ml,securitySolution,infra,monitoring,logstash,apm,uptime,bfetch,canvas]\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:28Z\",\"tags\":[\"info\",\"plugins\",\"taskManager\",\"taskManager\"],\"pid\":7,\"message\":\"TaskManager is identified by the Kibana UUID: bf01f365-e094-4cde-940d-3e0db65fa22a\"}\nelasticsearch | {\"type\": \"server\", \"timestamp\": \"2026-04-23T07:56:28,778Z\", \"level\": \"INFO\", \"component\": \"o.e.c.m.MetadataIndexTemplateService\", \"cluster.name\": \"docker-cluster\", \"node.name\": \"e802ad473a4f\", \"message\": \"adding template [.management-beats] for index patterns [.management-beats]\", \"cluster.uuid\": \"8uh2w1CUSGyWYR_OvaKx6g\", \"node.id\": \"e2ZKzgw4Q4aCf2w5ljWr1A\" }\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:28Z\",\"tags\":[\"info\",\"plugins\",\"crossClusterReplication\"],\"pid\":7,\"message\":\"Your basic license does not support crossClusterReplication. Please upgrade your license.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:28Z\",\"tags\":[\"info\",\"plugins\",\"watcher\"],\"pid\":7,\"message\":\"Your basic license does not support watcher. Please upgrade your license.\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:28Z\",\"tags\":[\"info\",\"plugins\",\"monitoring\",\"monitoring\",\"kibana-monitoring\"],\"pid\":7,\"message\":\"Starting monitoring stats collection\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:29Z\",\"tags\":[\"error\",\"elasticsearch\",\"data\"],\"pid\":7,\"message\":\"[version_conflict_engine_exception]: [task:Lens-lens_telemetry]: version conflict, document already exists (current version [745])\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:29Z\",\"tags\":[\"error\",\"elasticsearch\",\"data\"],\"pid\":7,\"message\":\"[version_conflict_engine_exception]: [task:Actions-actions_telemetry]: version conflict, document already exists (current version [745])\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:29Z\",\"tags\":[\"error\",\"elasticsearch\",\"data\"],\"pid\":7,\"message\":\"[version_conflict_engine_exception]: [task:endpoint:user-artifact-packager:1.0.0]: version conflict, document already exists (current version [310913])\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:29Z\",\"tags\":[\"error\",\"elasticsearch\",\"data\"],\"pid\":7,\"message\":\"[version_conflict_engine_exception]: [task:Alerting-alerting_telemetry]: version conflict, document already exists (current version [745])\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:29Z\",\"tags\":[\"error\",\"elasticsearch\",\"data\"],\"pid\":7,\"message\":\"[version_conflict_engine_exception]: [task:apm-telemetry-task]: version conflict, document already exists (current version [1222])\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:29Z\",\"tags\":[\"listening\",\"info\"],\"pid\":7,\"message\":\"Server running at http://0:5601\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:31Z\",\"tags\":[\"info\",\"http\",\"server\",\"Kibana\"],\"pid\":7,\"message\":\"http server running at http://0:5601\"}\nkibana | {\"type\":\"log\",\"@timestamp\":\"2026-04-23T07:56:32Z\",\"tags\":[\"warning\",\"plugins\",\"reporting\"],\"pid\":7,\"message\":\"Enabling the Chromium sandbox provides an additional layer of protection.\"}\n\n\nv View in Docker Desktop o View Config w Enable Watch","is_focused":true},{"role":"AXButton","text":"Menu","depth":3,"bounds":{"left":0.48472223,"top":0.08944444,"width":0.010416667,"height":0.016666668},"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"⌥1 DOCKER (docker-compose)","depth":3,"bounds":{"left":0.01875,"top":0.09,"width":0.4625,"height":0.015555556},"role_description":"text"},{"role":"AXTextArea","text":"Last login: Mon Apr 20 19:47:56 on console\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ prod\n(lukas@jiminny-prod-bastion) Verification code: \nWelcome to Ubuntu 22.04.5 LTS (GNU/Linux 6.8.0-1041-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Tue Apr 21 06:17:09 UTC 2026\n\n System load: 0.0 Processes: 126\n Usage of /: 58.6% of 7.57GB Users logged in: 2\n Memory usage: 36% IPv4 address for eth0: 10.30.45.167\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n37 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\n*** System restart required ***\nLast login: Mon Apr 20 15:14:15 2026 from 212.5.153.87\nlukas@jiminny-prod-bastion:~$ client_loop: send disconnect: Broken pipe\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ prod\n(lukas@jiminny-prod-bastion) Verification code: \nWelcome to Ubuntu 22.04.5 LTS (GNU/Linux 6.8.0-1041-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Wed Apr 22 08:09:37 UTC 2026\n\n System load: 0.0 Processes: 158\n Usage of /: 58.7% of 7.57GB Users logged in: 7\n Memory usage: 41% IPv4 address for eth0: 10.30.45.167\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n37 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\n*** System restart required ***\nLast login: Tue Apr 21 06:17:09 2026 from 212.5.153.87\nlukas@jiminny-prod-bastion:~$ client_loop: send disconnect: Broken pipe\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ prod\n(lukas@jiminny-prod-bastion) Verification code: \nWelcome to Ubuntu 22.04.5 LTS (GNU/Linux 6.8.0-1041-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Wed Apr 22 12:09:08 UTC 2026\n\n System load: 0.0 Processes: 168\n Usage of /: 58.8% of 7.57GB Users logged in: 7\n Memory usage: 43% IPv4 address for eth0: 10.30.45.167\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n37 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\n*** System restart required ***\nLast login: Wed Apr 22 08:09:38 2026 from 212.5.153.87\nlukas@jiminny-prod-bastion:~$ client_loop: send disconnect: Broken pipe\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","depth":5,"value":"Last login: Mon Apr 20 19:47:56 on console\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ prod\n(lukas@jiminny-prod-bastion) Verification code: \nWelcome to Ubuntu 22.04.5 LTS (GNU/Linux 6.8.0-1041-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Tue Apr 21 06:17:09 UTC 2026\n\n System load: 0.0 Processes: 126\n Usage of /: 58.6% of 7.57GB Users logged in: 2\n Memory usage: 36% IPv4 address for eth0: 10.30.45.167\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n37 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\n*** System restart required ***\nLast login: Mon Apr 20 15:14:15 2026 from 212.5.153.87\nlukas@jiminny-prod-bastion:~$ client_loop: send disconnect: Broken pipe\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ prod\n(lukas@jiminny-prod-bastion) Verification code: \nWelcome to Ubuntu 22.04.5 LTS (GNU/Linux 6.8.0-1041-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Wed Apr 22 08:09:37 UTC 2026\n\n System load: 0.0 Processes: 158\n Usage of /: 58.7% of 7.57GB Users logged in: 7\n Memory usage: 41% IPv4 address for eth0: 10.30.45.167\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n37 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\n*** System restart required ***\nLast login: Tue Apr 21 06:17:09 2026 from 212.5.153.87\nlukas@jiminny-prod-bastion:~$ client_loop: send disconnect: Broken pipe\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ prod\n(lukas@jiminny-prod-bastion) Verification code: \nWelcome to Ubuntu 22.04.5 LTS (GNU/Linux 6.8.0-1041-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Wed Apr 22 12:09:08 UTC 2026\n\n System load: 0.0 Processes: 168\n Usage of /: 58.8% of 7.57GB Users logged in: 7\n Memory usage: 43% IPv4 address for eth0: 10.30.45.167\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n37 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\n*** System restart required ***\nLast login: Wed Apr 22 08:09:38 2026 from 212.5.153.87\nlukas@jiminny-prod-bastion:~$ client_loop: send disconnect: Broken pipe\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","is_focused":true},{"role":"AXButton","text":"Menu","depth":4,"bounds":{"left":0.98541665,"top":0.08944444,"width":0.010416667,"height":0.016666668},"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"⌥2 PROD (-zsh)","depth":4,"bounds":{"left":0.51875,"top":0.09,"width":0.46319443,"height":0.015555556},"role_description":"text"},{"role":"AXTextArea","text":"Last login: Mon Apr 20 19:48:03 on ttys001\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ eu\n(lukas@jiminny-eu-bastion) Verification code: \nWelcome to Ubuntu 22.04.4 LTS (GNU/Linux 6.8.0-1047-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Tue Apr 21 06:17:15 UTC 2026\n\n System load: 0.0 Processes: 119\n Usage of /: 58.0% of 7.57GB Users logged in: 2\n Memory usage: 19% IPv4 address for eth0: 10.20.163.228\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n85 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\n*** System restart required ***\nLast login: Mon Apr 20 15:14:23 2026 from 212.5.153.87\nlukas@jiminny-eu-bastion:~$ client_loop: send disconnect: Broken pipe\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ eu\n(lukas@jiminny-eu-bastion) Verification code: \nWelcome to Ubuntu 22.04.4 LTS (GNU/Linux 6.8.0-1047-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Tue Apr 21 16:24:08 UTC 2026\n\n System load: 0.0 Processes: 139\n Usage of /: 58.0% of 7.57GB Users logged in: 3\n Memory usage: 21% IPv4 address for eth0: 10.20.163.228\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n85 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\n*** System restart required ***\nLast login: Tue Apr 21 06:17:16 2026 from 212.5.153.87\nlukas@jiminny-eu-bastion:~$ client_loop: send disconnect: Broken pipe\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ eu\n(lukas@jiminny-eu-bastion) Verification code: \nWelcome to Ubuntu 22.04.4 LTS (GNU/Linux 6.8.0-1047-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Wed Apr 22 08:09:45 UTC 2026\n\n System load: 0.0 Processes: 133\n Usage of /: 58.7% of 7.57GB Users logged in: 4\n Memory usage: 21% IPv4 address for eth0: 10.20.163.228\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n85 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\n*** System restart required ***\nLast login: Tue Apr 21 16:24:08 2026 from 212.5.153.87\nlukas@jiminny-eu-bastion:~$","depth":5,"value":"Last login: Mon Apr 20 19:48:03 on ttys001\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ eu\n(lukas@jiminny-eu-bastion) Verification code: \nWelcome to Ubuntu 22.04.4 LTS (GNU/Linux 6.8.0-1047-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Tue Apr 21 06:17:15 UTC 2026\n\n System load: 0.0 Processes: 119\n Usage of /: 58.0% of 7.57GB Users logged in: 2\n Memory usage: 19% IPv4 address for eth0: 10.20.163.228\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n85 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\n*** System restart required ***\nLast login: Mon Apr 20 15:14:23 2026 from 212.5.153.87\nlukas@jiminny-eu-bastion:~$ client_loop: send disconnect: Broken pipe\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ eu\n(lukas@jiminny-eu-bastion) Verification code: \nWelcome to Ubuntu 22.04.4 LTS (GNU/Linux 6.8.0-1047-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Tue Apr 21 16:24:08 UTC 2026\n\n System load: 0.0 Processes: 139\n Usage of /: 58.0% of 7.57GB Users logged in: 3\n Memory usage: 21% IPv4 address for eth0: 10.20.163.228\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n85 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\n*** System restart required ***\nLast login: Tue Apr 21 06:17:16 2026 from 212.5.153.87\nlukas@jiminny-eu-bastion:~$ client_loop: send disconnect: Broken pipe\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ eu\n(lukas@jiminny-eu-bastion) Verification code: \nWelcome to Ubuntu 22.04.4 LTS (GNU/Linux 6.8.0-1047-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Wed Apr 22 08:09:45 UTC 2026\n\n System load: 0.0 Processes: 133\n Usage of /: 58.7% of 7.57GB Users logged in: 4\n Memory usage: 21% IPv4 address for eth0: 10.20.163.228\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n85 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\n*** System restart required ***\nLast login: Tue Apr 21 16:24:08 2026 from 212.5.153.87\nlukas@jiminny-eu-bastion:~$","is_focused":true},{"role":"AXButton","text":"Menu","depth":4,"bounds":{"left":0.98541665,"top":0.23944445,"width":0.010416667,"height":0.016666668},"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"⌥3 EU (ssh)","depth":4,"bounds":{"left":0.51875,"top":0.24,"width":0.46319443,"height":0.015555556},"role_description":"text"},{"role":"AXTextArea","text":"Last login: Mon Apr 20 19:48:04 on ttys002\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ stg\n(lukas@jiminny-stage-bastion) Verification code: \nWelcome to Ubuntu 22.04.4 LTS (GNU/Linux 6.8.0-1052-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Tue Apr 21 14:39:05 UTC 2026\n\n System load: 0.0 Processes: 137\n Usage of /: 57.6% of 7.57GB Users logged in: 4\n Memory usage: 30% IPv4 address for ens5: 10.30.46.154\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n85 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\nLast login: Thu Apr 16 07:34:39 2026 from 212.39.71.189\nlukas@jiminny-stage-bastion:~$ client_loop: send disconnect: Broken pipe\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","depth":5,"value":"Last login: Mon Apr 20 19:48:04 on ttys002\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ stg\n(lukas@jiminny-stage-bastion) Verification code: \nWelcome to Ubuntu 22.04.4 LTS (GNU/Linux 6.8.0-1052-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Tue Apr 21 14:39:05 UTC 2026\n\n System load: 0.0 Processes: 137\n Usage of /: 57.6% of 7.57GB Users logged in: 4\n Memory usage: 30% IPv4 address for ens5: 10.30.46.154\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n85 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\nLast login: Thu Apr 16 07:34:39 2026 from 212.39.71.189\nlukas@jiminny-stage-bastion:~$ client_loop: send disconnect: Broken pipe\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","is_focused":true},{"role":"AXButton","text":"Menu","depth":4,"bounds":{"left":0.98541665,"top":0.40944445,"width":0.010416667,"height":0.016666668},"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"⌥4 STAGE (-zsh)","depth":4,"bounds":{"left":0.51875,"top":0.41,"width":0.46319443,"height":0.015555556},"role_description":"text"},{"role":"AXTextArea","text":"Last login: Mon Apr 20 19:48:04 on ttys002\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","depth":5,"bounds":{"left":0.5,"top":0.56222224,"width":0.5,"height":0.1388889},"value":"Last login: Mon Apr 20 19:48:04 on ttys002\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","is_focused":true},{"role":"AXButton","text":"Menu","depth":4,"bounds":{"left":0.98541665,"top":0.5594444,"width":0.010416667,"height":0.016666668},"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"⌥5 QA (-zsh)","depth":4,"bounds":{"left":0.51875,"top":0.56,"width":0.46319443,"height":0.015555556},"role_description":"text"},{"role":"AXTextArea","text":"Last login: Mon Apr 20 19:48:04 on ttys004\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","depth":5,"bounds":{"left":0.5,"top":0.7088889,"width":0.5,"height":0.1411111},"value":"Last login: Mon Apr 20 19:48:04 on ttys004\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","is_focused":true},{"role":"AXButton","text":"Menu","depth":4,"bounds":{"left":0.98541665,"top":0.70611113,"width":0.010416667,"height":0.016666668},"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"⌥6 FE (-zsh)","depth":4,"bounds":{"left":0.51875,"top":0.70666665,"width":0.46319443,"height":0.015555556},"role_description":"text"},{"role":"AXTextArea","text":"Last login: Mon Apr 20 19:48:04 on ttys005\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","depth":5,"bounds":{"left":0.5,"top":0.87777776,"width":0.5,"height":0.12222222},"value":"Last login: Mon Apr 20 19:48:04 on ttys005\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","is_focused":true},{"role":"AXButton","text":"Menu","depth":4,"bounds":{"left":0.98541665,"top":0.855,"width":0.010416667,"height":0.016666668},"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"⌥7 EXT (-zsh)","depth":4,"bounds":{"left":0.51875,"top":0.85555553,"width":0.46319443,"height":0.015555556},"role_description":"text"},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.12291667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.12708333,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.24583334,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.25,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"✳ Build full day activity summary from Screenpipe (claude)","depth":2,"bounds":{"left":0.36875,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.37291667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.49166667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.49583334,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.6145833,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.61875,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.7375,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.7416667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ec2-user@ip-10-30-159-186:~ (-zsh)","depth":2,"bounds":{"left":0.86041665,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8645833,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"DOCKER (docker-compose)","depth":1,"bounds":{"left":0.43611112,"top":0.033333335,"width":0.12708333,"height":0.017777778},"role_description":"text"}]...
|
-3009288458992913716
|
-8305438663760624882
|
visual_change
|
accessibility
|
NULL
|
0 social account(s) to be processed ...
docker_lam 0 social account(s) to be processed ...
docker_lamp_1 |
docker_lamp_1 | Done!
docker_lamp_1 | 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:bullhorn:ping --heartbeat > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 |
docker_lamp_1 | 2026-04-22 15:41:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:41:04 Running ['artisan' dialers:monitor-activities] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:41:06 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:41:07 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:41:08 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:41:10 Running ['artisan' crm:sync-hubspot-objects] [HubSpot] Syncing objects for Hubspot (abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4) since 2026-04-22 15:36:20 (delay: 0s)
docker_lamp_1 | Team TestV (2708d27a-7f31-4d90-bd2b-b5bde1c0211e) is not yet assigned an owner. skipping...
docker_lamp_1 | Team KioskAccount (dedab245-604c-4cd2-a83e-4b9e034b2772) is not yet assigned an owner. skipping...
docker_lamp_1 | [HubSpot] Syncing objects for InsightSquared (b2b115eb-93ce-4d1b-929c-173757df8fba) since 2026-02-17 15:09:59 (delay: 0s)
docker_lamp_1 | Team Aircall Demo (7980e5eb-b11c-4cee-9c21-8bb29ba85f3b) is not yet assigned an owner. skipping...
docker_lamp_1 | [HubSpot] Syncing objects for GoStudent UAT (b2d49a54-b645-4637-a7ae-a86cfce6e8e4) since 2026-02-17 15:09:59 (delay: 1s)
docker_lamp_1 | [HubSpot] Syncing objects for JustCall (c6b9d6b0-b48d-4832-a68c-a57d60651888) since 2026-02-17 15:07:41 (delay: 1s)
docker_lamp_1 | Team Twilio Video (c334ca55-b230-411c-b10e-31c8204bd07b) is not yet assigned an owner. skipping...
docker_lamp_1 | Team My Test Account 3000 (dbc9990d-b35f-4e38-9550-22cdd6059514) is not yet assigned an owner. skipping...
docker_lamp_1 | Team test (7997eb70-8aa4-491a-870d-311977568df4) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Test (5e06dcee-0613-470e-9a77-2c283198f3bf) is not yet assigned an owner. skipping...
docker_lamp_1 | Team test ogg auto sync (da44776e-306f-427a-83d8-a1b4baa5537e) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Tourlaner (d9b71080-388b-4cf5-8175-aa0f29bee635) is not yet assigned an owner. skipping...
docker_lamp_1 | Dispatched 4 HubSpot sync jobs
docker_lamp_1 | 2026-04-22 15:41:11 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:sync-hubspot-objects > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | 2026-04-22 15:41:11 Jiminny\Jobs\Crm\SyncHubspotObjects ....... 44.08ms DONE
docker_lamp_1 | 2026-04-22 15:41:11 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 | 2026-04-22 15:41:12 Jiminny\Jobs\Crm\SyncHubspotObjects ...... 577.01ms DONE
docker_lamp_1 | 2026-04-22 15:41:12 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:41:12 Jiminny\Jobs\Crm\SyncHubspotObjects ....... 16.42ms DONE
docker_lamp_1 | 2026-04-22 15:41:12 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:41:12 Jiminny\Jobs\Crm\SyncHubspotObjects ....... 17.23ms DONE
docker_lamp_1 |
docker_lamp_1 | 2026-04-22 15:42:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:42:03 Running ['artisan' dialers:monitor-activities] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:42:05 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:42:06 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:42:07 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:42:09 Running ['artisan' conference:monitor:count] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:42:10 Running ['artisan' mailbox:batch:create] ....... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:create > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 | 2026-04-22 15:42:14 Jiminny\Jobs\Mailbox\CreateBatches [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:42:14 Jiminny\Jobs\Mailbox\CreateBatches [PASSWORD_DOTS] 87.60ms DONE
docker_lamp_1 |
docker_lamp_1 | 2026-04-22 15:43:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:43:04 Running ['artisan' dialers:monitor-activities] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:43:06 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:43:08 Running ['artisan' mailbox:skip-lists:refresh] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:43:10 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:43:11 Running ['artisan' mailbox:batch:retry-failed --max-batches=15] in background 5.15ms DONE
docker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' mailbox:batch:retry-failed --max-batches=15 > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish "framework/schedule-390defd641effba0f73a895e426ded4cf2ba7f11" "$?") > '/dev/null' 2>&1 &
docker_lamp_1 | 2026-04-22 15:43:11 Running ['artisan' calendar:sync --dateMode=daily] 2026-04-22 15:43:19 Jiminny\Jobs\Calendar\SyncCalendarEvents ....... RUNNING
docker_lamp_1 | 2026-04-22 15:43:20 Jiminny\Jobs\Calendar\SyncCalendarEvents ....... 1s DONE
docker_lamp_1 | 2026-04-22 15:43:22 Jiminny\Jobs\Calendar\SyncCalendarEvents ....... RUNNING
docker_lamp_1 | 10s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' calendar:sync --dateMode=daily > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 | 2026-04-22 15:43:23 Jiminny\Jobs\Calendar\SyncCalendarEvents . 596.48ms DONE
docker_lamp_1 | 2026-04-22 15:43:23 Jiminny\Jobs\Calendar\SyncCalendarEvents ....... RUNNING
docker_lamp_1 | 2026-04-22 15:43:23 Jiminny\Jobs\Calendar\SyncCalendarEvents .. 52.11ms DONE
docker_lamp_1 |
docker_lamp_1 | 2026-04-22 15:44:02 Running ['artisan' meeting-bot:schedule-bot] ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:44:04 Running ['artisan' dialers:monitor-activities] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:44:05 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:44:07 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:44:09 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:44:11 Running ['artisan' conference:monitor:count] ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:44:13 Running ['artisan' crm:sync-objects] Syncing objects for Salesforce Team (6473c918-d8db-4ded-a52b-4febfd7b7c02) since 2026-03-19 20:14:10 (delay: 0s)
docker_lamp_1 | Team Edge Communications (c3a725ef-fc5e-4c7b-9231-6379b2834101) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Burlington Textiles Corp of America (72c0ccae-28a5-44bd-a0b4-958e73135463) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Uber (1c523aed-80e3-4c44-b215-7f0b027a03a2) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Vonage (c6030540-121e-4d6b-9261-8d82105817bf) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Nexmo (eaba09e1-4973-4077-b041-ad389248318c) is not yet assigned an owner. skipping...
docker_lamp_1 | Team SF (018e2d7c-ed01-4af2-be33-5c28f454a185) is not yet assigned an owner. skipping...
docker_lamp_1 | Team NewAccount (27325244-25d7-4431-aac4-9ddb2bde6b45) is not yet assigned an owner. skipping...
docker_lamp_1 | Syncing objects for Pipedrive, Inc. (51467630-d89d-480b-be20-933e64a042f7) since 2026-04-08 18:14:13 (delay: 2s)
docker_lamp_1 | Syncing objects for Copper (396ed57c-e3c4-49be-8290-37c32955f7c7) since 2026-04-22 15:14:16 (delay: 4s)
docker_lamp_1 | Syncing objects for Pipedrive External Test (fda3cbdf-1117-4ba5-86f8-775f548b3a28) since 2026-04-08 18:14:16 (delay: 6s)
docker_lamp_1 | Syncing objects for Close (3ff5a02a-86fb-4357-b1d6-a04e26c38602) since 2026-04-22 15:14:20 (delay: 8s)
docker_lamp_1 | Team DeewanyAccount (cfaf90cb-ecf0-432c-a655-a22d9985e4c8) is not yet assigned an owner. skipping...
docker_lamp_1 | Syncing objects for Bullhorn (1640a0ac-19da-4c3b-90f7-87525f07a6d2) since 2026-02-19 13:28:05 (delay: 10s)
docker_lamp_1 | Team Horen test (93d8f8f2-dd61-4137-97de-a02a9d3751bb) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Ahmed Hamadi (191379e4-5e90-4419-8ffd-4804a5abb565) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Ahmed Testing (3b2e1fad-bc5f-4322-84d2-a7440f78c5d7) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Ahmed jiminny (1dbd47b8-bbf8-4bbb-8e28-67b9c91f566e) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Testers Inc (ec0767d5-1248-4447-b316-94dd9a30d52b) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Copper Team (817c8ce1-8e22-4ac8-8fca-8df1131963ab) is not yet assigned an owner. skipping...
docker_lamp_1 | Team TheBestPlace Ever (dbb998f9-70eb-4f11-a9f2-d9f6157f4035) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Horen's apartments (e10bb4ea-6c89-4fe3-be3b-0a278aa81180) is not yet assigned an owner. skipping...
docker_lamp_1 | Team New Org Test (2cb527b6-73a4-46be-b7b2-b0097b494cc5) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Pipedrive test org (18ea22be-6e0b-461c-a4db-536c022aac73) is not yet assigned an owner. skipping...
docker_lamp_1 | Team BigChairs Inc. (16dcfaf9-44b4-4cc2-9ead-f98dd0cad4dd) is not yet assigned an owner. skipping...
docker_lamp_1 | Syncing objects for GL 500 (0c33bf2d-1c77-4200-8ed6-6147ad444c30) since 2026-02-19 13:26:04 (delay: 12s)
docker_lamp_1 | Team Loren X (243d7d4b-1849-46c2-9dfb-4462f52020ef) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Laravel Company 2024 (e466d657-c26d-4281-9ed5-73890fe711f6) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Barbara Fuchs (bea0fcf3-f8f9-471d-92c4-d1b7d1a2e228) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Test salesforce auto sync (2c2e652e-1cbb-4620-852d-e73bef0ab4a9) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Bullhorn (83c14120-cdb1-42e9-b0ac-16ef11320a8f) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Globo Tech (fb8d5211-3c4d-4804-9223-b091670ffd79) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Laravel 11 Company 2024 (780c6836-764b-4e47-9a05-cfefbab6e590) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Nikon7 (e9bff902-edc3-4b0b-89b1-2a9aa43d940a) is not yet assigned an owner. skipping...
docker_lamp_1 | Syncing objects for Dev Zoho CRM client (1ece66c8-feb1-4df1-b321-21607daf4623) since 2026-04-22 15:14:26 (delay: 14s)
docker_lamp_1 | [PASSWORD_DOTS] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:sync-objects > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 | 2026-04-22 15:44:15 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:15 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 49.60ms DONE
docker_lamp_1 | 2026-04-22 15:44:15 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob RUNNING
docker_lamp_1 | 2026-04-22 15:44:16 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob 601.82ms DONE
docker_lamp_1 | 2026-04-22 15:44:17 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:17 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 24.34ms DONE
docker_lamp_1 | 2026-04-22 15:44:19 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:20 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 1s DONE
docker_lamp_1 | 2026-04-22 15:44:20 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob RUNNING
docker_lamp_1 | 2026-04-22 15:44:21 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob 557.11ms DONE
docker_lamp_1 | 2026-04-22 15:44:21 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:21 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 58.10ms DONE
docker_lamp_1 | 2026-04-22 15:44:23 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:24 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 1s DONE
docker_lamp_1 | 2026-04-22 15:44:25 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob RUNNING
docker_lamp_1 | 2026-04-22 15:44:26 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob 787.71ms DONE
docker_lamp_1 | 2026-04-22 15:44:26 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:26 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 18.07ms DONE
docker_lamp_1 | 2026-04-22 15:44:27 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:27 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 19.18ms DONE
docker_lamp_1 | 2026-04-22 15:44:29 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-04-22 15:44:33 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 3s DONE
docker_lamp_1 | 2026-04-22 15:44:33 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob RUNNING
docker_lamp_1 | 2026-04-22 15:44:34 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob 848.26ms DONE
docker_lamp_1 | 2026-04-22 15:44:35 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob RUNNING
docker_lamp_1 | 2026-04-22 15:44:35 Jiminny\Jobs\Crm\Salesforce\FetchSalesforceEntitiesJob 431.28ms DONE
docker_lamp_1 |
docker_lamp_1 | 2026-04-22 15:45:02 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:03 Running ['artisan' dialers:monitor-activities] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:06 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:09 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:10 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:12 Running ['artisan' activity:purge-stale] ....... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:purge-stale > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:15 Running ['artisan' mailbox:text-relay:sync] {
docker_lamp_1 | "error": "invalid_request",
docker_lamp_1 | "error_description": "Invalid impersonation \u0026quot;sub\u0026quot; field: @"
docker_lamp_1 | }
docker_lamp_1 | .... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:text-relay:sync > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:17 Running ['artisan' conference:pre-meeting-notification] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:pre-meeting-notification > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-04-22 15:45:19 Running ['artisan' conference:monitor:start] ... 4s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:start > '/proc/1/fd/1' 2>&1
unexpected EOF
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/infrastructure/dev/docker (develop) $ work
WARN[0000] /Users/lukas/jiminny/infrastructure/dev/docker/docker-compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion
Attaching to blackfire-1, datadog-1, jiminny_ext-1, mariadb-1, docker_lamp_1, elasticsearch, kibana, ngrok, redis
redis | 1:C 23 Apr 2026 07:55:51.731 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
redis | 1:C 23 Apr 2026 07:55:51.731 # Redis version=5.0.14, bits=64, commit=00000000, modified=0, pid=1, just started
redis | 1:C 23 Apr 2026 07:55:51.731 # Configuration loaded
mariadb-1 | 2026-04-23 07:55:51+00:00 [Note] [Entrypoint]: Entrypoint script for MariaDB Server 1:11.4.5+maria~ubu2404 started.
redis | 1:M 23 Apr 2026 07:55:51.741 * Running mode=standalone, port=6379.
redis | 1:M 23 Apr 2026 07:55:51.741 # Server initialized
redis | 1:M 23 Apr 2026 07:55:51.741 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled.
redis | 1:M 23 Apr 2026 07:55:51.744 * Reading RDB preamble from AOF file...
redis | 1:M 23 Apr 2026 07:55:51.745 * Reading the remaining AOF tail...
blackfire-1 | [2026-04-23T07:55:51Z] ERROR: The server ID parameter is not set. Please run 'blackfire-agent -register' to configure it.
blackfire-1 | usage blackfire-agent [options]
blackfire-1 | --collector="[URL_WITH_CREDENTIALS] controller (64 bit): Version 7.10.2 (Build 40a3af639d4698) Copyright (c) 2020 Elasticsearch BV" }
elasticsearch | {"type": "server", "timestamp": "2026-04-23T07:56:08,335Z", "level": "INFO", "component": "o.e.t.NettyAllocator", "cluster.name": "docker-cluster", "node.name": "e802ad473a4f", "message": "creating NettyAllocator with the following configs: [name=unpooled, suggested_max_allocation_size=256kb, factors={es.unsafe.use_unpooled_allocator=null, g1gc_enabled=true, g1gc_region_size=1mb, heap_size=700mb}]" }
elasticsearch | {"type": "server", "timestamp": "2026-04-23T07:56:08,444Z", "level": "INFO", "component": "o.e.d.DiscoveryModule", "cluster.name": "docker-cluster", "node.name": "e802ad473a4f", "message": "using discovery type [single-node] and seed hosts providers [settings]" }
elasticsearch | {"type": "server", "timestamp": "2026-04-23T07:56:08,962Z", "level": "WARN", "component": "o.e.g.DanglingIndicesState", "cluster.name": "docker-cluster", "node.name": "e802ad473a4f", "message": "gateway.auto_import_dangling_indices is disabled, dangling indices will not be automatically detected or imported and must be managed manually" }
elasticsearch | {"type": "server", "timestamp": "2026-04-23T07:56:09,262Z", "level": "INFO", "component": "o.e.n.Node", "cluster.name": "docker-cluster", "node.name": "e802ad473a4f", "message": "initialized" }
elasticsearch | {"type": "server", "timestamp": "2026-04-23T07:56:09,263Z", "level": "INFO", "component": "o.e.n.Node", "cluster.name": "docker-cluster", "node.name": "e802ad473a4f", "message": "starting ..." }
elasticsearch | {"type": "server", "timestamp": "2026-04-23T07:56:09,430Z", "level": "INFO", "component": "o.e.t.TransportService", "cluster.name": "docker-cluster", "node.name...
|
73566
|
|
73514
|
NULL
|
0
|
2026-04-23T07:51:35.343757+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776930695343_m2.jpg...
|
PhpStorm
|
faVsco.js – JiminnyDebugCommand.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
111
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands;
use Carbon\Carbon;
use Illuminate\Console\Command;
use InvalidArgumentException;
use Jiminny\Jobs\AutomatedReports\RequestGenerateAskJiminnyReportJob;
use Jiminny\Jobs\AutomatedReports\SendReportMailJob;
use Jiminny\Jobs\JobDispatcherInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
/**
* Class JiminnyDebugCommand
*
* @package Jiminny\Console\Commands
*/
class JiminnyDebugCommand extends Command
{
public const string FREQUENCY_DAILY = 'daily';
public const string FREQUENCY_WEEKLY = 'weekly';
public const string FREQUENCY_MONTHLY = 'monthly';
public const string FREQUENCY_QUARTERLY = 'quarterly';
public const string FREQUENCY_ONE_OFF = 'one_off';
protected $signature = 'jiminny:debug';
public function handle(JobDispatcherInterface $jobDispatcher, AutomatedReportsService $automatedReportsService): void
{
exit(1);
$now = Carbon::now()->subDay(5);
$from = $now->copy()->previousWeekday()->startOfDay();
$to = $now->copy()->previousWeekday()->endOfDay();
$this->info("Now: {$now->toDateTimeString()}");
$this->info("From: {$from->toDateTimeString()}");
$this->info("To: {$to->toDateTimeString()}");
exit(1);
$report = AutomatedReport::find(71);
$job = new RequestGenerateAskJiminnyReportJob($report->getUuid());
$jobDispatcher->dispatch($job);
exit(1);
// $this->formatDate($jobDispatcher);
// $this->sendMail($jobDispatcher, $automatedReportsService);
// $this->crmService();
$this->getPayload($automatedReportsService);
exit(1);
}
private function crmService()
{
$activity = Activity::find(418141);
$team = Team::find(19);
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
$crmService->createTranscriptNotes($activity);
}
private function sendMail(JobDispatcherInterface $jobDispatcher, AutomatedReportsService $automatedReportsService)
{
$reportUuid = '';
// $report = $automatedReportsService->getReportResult($reportUuid);
$report = AutomatedReportResult::find(275);
$validRecipients = $automatedReportsService->getValidRecipientUsers(
$report->getReport(),
includeJiminny: true,
);
$recipient = $validRecipients[0];
$fileName = $automatedReportsService->getReportFileName($report);
$typeName = $report->getReport()->getCustomName()
?? $automatedReportsService->getReportTypeName($report);
$teamsName = $automatedReportsService->getReportTeamsName($report);
$periodName = $automatedReportsService->getReportPeriodName($report);
$s3Path = $automatedReportsService->getMediaPath($report);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$fileName ' . PHP_EOL . print_r($fileName, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$typeName ' . PHP_EOL . print_r($typeName, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$teamsName ' . PHP_EOL . print_r($teamsName, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$periodName ' . PHP_EOL . print_r($periodName, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$s3Path ' . PHP_EOL . print_r($s3Path, true));
$jobDispatcher->dispatch(
new SendReportMailJob(
reportUuid: $report->getUuid(),
s3Path: $s3Path,
recipientEmail: $recipient['email'],
recipientName: $recipient['name'] ?? null,
fileName: $fileName,
typeName: $typeName,
teamsName: $teamsName,
periodName: $periodName,
isAskJiminny: true,
)
);
exit(1);
}
private function formatDate(JobDispatcherInterface $jobDispatcher): void
{
$customName = 'Custom report name';
// $frequency = self::FREQUENCY_DAILY;
// $frequency = self::FREQUENCY_WEEKLY;
$frequency = self::FREQUENCY_MONTHLY;
// $frequency = self::FREQUENCY_QUARTERLY;
// $frequency = self::FREQUENCY_ONE_OFF;
$period = $this->calculateFromAndToDatePeriod($frequency);
$from = $period['fromDate'];
$to = $period['toDate'];
$periodName = $this->formatReportPeriodName($frequency, $from, $to);
$filenameSuffix = null;
if ($customName) {
if ($filenameSuffix) {
$customName .= " {$filenameSuffix}";
}
$result = $this->sanitizeFileName("{$customName} - {$periodName}");
}
$this->info($result);
}
public function calculateFromAndToDatePeriod(
string $frequency,
?Carbon $fromDate = null,
?Carbon $toDate = null
): array {
if ($frequency === self::FREQUENCY_ONE_OFF) {
return [
'fromDate' => $fromDate,
'toDate' => $toDate,
];
}
$now = Carbon::now();
return match ($frequency) {
self::FREQUENCY_DAILY => [
'fromDate' => $now->copy()->subDay()->startOfDay(),
'toDate' => $now->copy()->subDay()->endOfDay(),
],
self::FREQUENCY_WEEKLY => [
'fromDate' => $now->copy()->subWeeks(1)->startOfDay(),
'toDate' => $now->copy()->subDay()->endOfDay(),
],
self::FREQUENCY_MONTHLY => [
'fromDate' => $now->copy()->subMonths(1)->startOfDay(),
'toDate' => $now->copy()->subDay()->endOfDay(),
],
self::FREQUENCY_QUARTERLY => [
'fromDate' => $now->copy()->subMonths(3)->startOfDay(),
'toDate' => $now->copy()->subDay()->endOfDay(),
],
default => throw new InvalidArgumentException("Unsupported frequency: {$frequency}"),
};
}
private function formatReportPeriodName(string $frequency, Carbon $from, Carbon $to): string
{
$fromYear = $from->format('Y');
$toYear = $to->format('Y');
$differentYears = $fromYear !== $toYear;
switch ($frequency) {
case self::FREQUENCY_DAILY:
return $from->format('j M Y');
case self::FREQUENCY_QUARTERLY:
// 'Jan-Mar 2025' or 'Nov 2024-Jan 2025' if years differ
$startMonth = $from->format('M');
$endMonth = $to->copy()->subMonth();
$endMonthName = $endMonth->format('M');
$endMonthYear = $endMonth->format('Y');
if ($differentYears) {
return "{$startMonth} {$fromYear} - {$endMonthName} {$endMonthYear}";
}
return "{$startMonth} - {$endMonthName} {$toYear}";
case self::FREQUENCY_MONTHLY:
// 'May 2025' - monthly reports are always within the same year
return $from->format('M Y');
case self::FREQUENCY_WEEKLY:
// '4 - 8 Aug 2025', '27 Oct - 3 Nov 2025', or '28 Dec 2024 - 3 Jan 2025' if years differ
$startDay = $from->format('j');
$endDay = $to->format('j');
$startMonth = $from->format('M');
$endMonth = $to->format('M');
if ($differentYears) {
return "{$startDay} {$startMonth} {$fromYear} - {$endDay} {$endMonth} {$toYear}";
}
if ($startMonth !== $endMonth) {
return "{$startDay} {$startMonth} - {$endDay} {$endMonth} {$toYear}";
}
return "{$startDay} - {$endDay} {$endMonth} {$toYear}";
case self::FREQUENCY_ONE_OFF:
// '2 May-31 May 2025' or '15 Dec 2024-15 Jan 2025' if years differ
$startDay = $from->format('j');
$startMonth = $from->format('M');
$endDay = $to->format('j');
$endMonth = $to->format('M');
// If same month and year, use a format like '2-31 May 2025'
if ($startMonth === $endMonth && ! $differentYears) {
return "{$startDay} - {$endDay} {$startMonth} {$toYear}";
}
// If different years, include both years
if ($differentYears) {
return "{$startDay} {$startMonth} {$fromYear} - {$endDay} {$endMonth} {$toYear}";
}
// Same year but different months
return "{$startDay} {$startMonth} - {$endDay} {$endMonth} {$toYear}";
default:
// Default format for unknown frequencies
return $from->format('j M Y') . ' - ' . $to->format('j M Y');
}
}
public function sanitizeFileName(string $fileName): string
{
return str_replace(['/', '\\'], '-', $fileName);
}
private function getPayload(AutomatedReportsService $automatedReportsService)
{
$reportResult = AutomatedReportResult::find(269);
$automatedReport = $reportResult->getReport();
$activityIds = [1,2,3];
$payload = $automatedReportsService->getAskJiminnyGenerateReportPayload(
automatedReport: $automatedReport,
reportResult: $reportResult,
activityIds: $activityIds,
);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$payload ' . PHP_EOL . print_r($payload, true));
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
36
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\DealInsights;
use Doctrine\DBAL\Connection;
use Generator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Jiminny\Component\DealInsights\Forecast\DealData;
use Jiminny\Component\DealInsights\Forecast\DealsFilter;
use Jiminny\Component\DealInsights\QueryBuilder\QueryBuilder;
use Jiminny\Component\DealInsights\QueryBuilder\Visitor\QueryBuilderVisitorInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Services\Crm\IntegrationApp\DTO\Utils\UrlGeneratorInterface;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class DealsRepository implements DealsRepositoryInterface
{
private Connection $connection;
private ProviderRegistry $providerRegistry;
/**
* @var QueryBuilderVisitorInterface[]
*/
private array $visitors = [];
/**
* @param QueryBuilderVisitorInterface[] $visitors
*/
public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])
{
$this->connection = $connection;
$this->providerRegistry = $crmProviderRegistry;
foreach ($visitors as $visitor) {
$this->visitors[$visitor->getIdentifier()] = $visitor;
}
}
public function getDeals(CriteriaInterface $criteria): array
{
$context = $criteria->getContext();
$team = $context->getTeam();
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$this->visit($qb, $criteria);
return $this->execute($team, $crmService, $qb);
}
public function getDeal(Team $team, int $id): array
{
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$qb->andWhere('opp.id = :id')->setParameter('id', $id);
return $this->execute($team, $crmService, $qb);
}
public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])
{
$qb = new QueryBuilder($this->connection);
$qb
->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')
->from('crm_fields', 'f')
->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')
->where('f.crm_configuration_id = :crm')
->andWhere('f.object_type = :type')
->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')
->orderBy('fd.object_id', 'ASC')
->addOrderBy('fd.updated_at', 'ASC')
->setParameter('type', Field::OBJECT_OPPORTUNITY)
->setParameter('crm', $crmId)
;
if (! empty($crmFields)) {
$fields = array_map(fn ($value): string => '"' . $value . '"', $crmFields);
$qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');
}
return $qb->executeQuery()->fetchAllAssociative();
}
public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAssociative();
}
public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('COALESCE(opp.currency_code, "' . $defaultCurrency . '") AS currency')
->addSelect('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
->groupBy('currency')
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAllAssociative();
}
public function getDealActivities(CriteriaInterface $criteria): array
{
$qb = Activity::with(['participants', 'user'])
->where('opportunity_id', $criteria->getOpportunityId())
->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())
->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())
->orderBy($criteria->getSortBy(), $criteria->getSortDirection())
;
// Should we filter activities by criteria? It's intended to filter deals.
return $qb->get()->all();
}
public function getStages(CriteriaInterface $criteria): array
{
$qb = new QueryBuilder($this->connection);
$qb
->select('id', 'label', 'sequence')
->from('stages', 's')
->where('crm_configuration_id = :crm_configuration_id')
->andWhere('type = :type')
->orderBy('sequence', 'ASC')
->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())
->setParameter('type', Stage::TYPE_OPPORTUNITY);
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$result[$row['id']] = [
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
public function getConfigurationStages(Configuration $configuration): Collection
{
return $configuration
->stages()
->where('type', Stage::TYPE_OPPORTUNITY)
->get();
}
public function getPipelineData(Configuration $crm): array
{
$qb = new QueryBuilder($this->connection);
$provider = $crm->provider;
$qb
->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')
->from('stages', 's')
->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')
->where('s.crm_configuration_id = :crm_configuration_id')
->andWhere('s.type = :type')
->orderBy('bps.business_process_id', 'ASC')
->addOrderBy('s.sequence', 'ASC')
->setParameter('crm_configuration_id', $crm->id)
->setParameter('type', Stage::TYPE_OPPORTUNITY)
;
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];
$result[$row['pipeline_id']][] = [
'value' => $value,
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
private function createQueryBuilder(string $realm): QueryBuilder
{
return (new QueryBuilder($this->connection))
->setRealm($realm)
->from('opportunities', 'opp')
->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')
->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')
->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')
;
}
/**
* Applies all applicable visitors and returns the IDs of the executed ones
*
* @return string[]
*/
private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array
{
$queryVisitors = [];
foreach ($this->visitors as $visitor) {
if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {
$visitor->visit($queryBuilder, $criteria);
$queryVisitors[] = $visitor->getIdentifier();
}
}
return $queryVisitors;
}
private function hydrateStages(array $deals): array
{
foreach ($this->fetchStages(array_keys($deals)) as $stage) {
$oppId = (int) $stage['opportunity_id'];
if (! isset($deals[$oppId])) {
continue; // or throw??!
}
$deals[$oppId]['stages'][] = [
'id' => $stage['stage_id'],
'name' => $stage['label'],
'enteredAt' => $stage['created_at'],
];
}
return $deals;
}
/**
* @param int[] $dealIds
*/
private function fetchStages(array $dealIds): array
{
if (empty($dealIds)) {
return [];
}
$qb = new QueryBuilder($this->connection);
$qb
->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')
->from('opportunity_stages', 'os')
->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')
->where($qb->expr()->in('os.opportunity_id', $dealIds))
->orderBy('os.opportunity_id', 'ASC')
->addOrderBy('s.created_at', 'ASC')
;
return $qb->executeQuery()->fetchAllAssociative();
}
private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array
{
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$data = [
'uuid' => RequiresUUID::toNormal($row['uuid']),
'name' => $row['name'],
'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),
'account' => [
'name' => $row['acc_name'],
'url' => $crmService->generateProviderUrl(
providerId: $row['acc_provider_id'],
objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'
),
],
'owner' => null,
'rawValue' => [
'amount' => (float) $row['value'],
'currency' => $row['currency_code'],
],
'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),
'openDate' => $row['remotely_created_at'] ?? null,
'closeDate' => $row['close_date'] ?? null,
'stages' => [],
'currentPipelineId' => $row['pipeline_id'],
'currentStage' => [
'id' => $row['stage_id'],
'enteredAt' => $row['stage_updated_at'],
],
'currentStageUpdatedAt' => $row['stage_updated_at'],
'isClosed' => (bool) $row['is_closed'],
'isWon' => (bool) $row['is_won'],
];
if (isset($row['owner_uuid'])) {
$data['owner'] = [
'uuid' => RequiresUUID::toNormal($row['owner_uuid']),
'name' => $row['owner_name'],
'photoUrl' => $row['owner_photo'] === null
? null
: client_cdn($row['owner_photo'], $team),
'id' => $row['owner_id'],
'job' => $row['owner_job'],
];
}
$result[(int) $row['opp_id']] = $data;
}
return $this->hydrateStages($result);
}
private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder
{
$qb = clone $queryBuilder;
$qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');
$qb
->select(...[
'opp.id as opp_id',
'opp.uuid',
'opp.name',
'opp.value',
'opp.currency_code',
'opp.close_date',
'opp.remotely_created_at',
'opp.is_closed',
'opp.is_won',
])
->addSelect(...[
'usr.uuid as owner_uuid',
'usr.name AS owner_name',
'usr.photo_path as owner_photo',
'usr.id AS owner_id',
'jt.name as owner_job',
])
->addSelect('opp.stage_id', 'opp.stage_updated_at')
->addSelect(...[
'acc.name AS acc_name',
'acc.is_internal as acc_is_internal',
'opp.stage_updated_at',
'acc.crm_provider_id AS acc_provider_id',
'opp.crm_provider_id AS opp_provider_id',
])
->addSelect('rt.business_process_id AS pipeline_id')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'));
return $qb;
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws SocialAccountTokenInvalidException
*/
private function getCrmService(Team $team): ServiceInterface
{
$crmService = $this->providerRegistry->get($team->crm->provider);
$crmService->setConfiguration($team->crm);
if ($crmService instanceof UrlGeneratorInterface) {
$crmService->setCrmUrlGenerator($team->crm);
}
return $crmService;
}
/**
*
* @return Generator<DealData>
*/
public function getForecastData(DealsFilter $filter): Generator
{
$opportunities = DB::query()
->select([
'o.value',
'o.close_date',
'o.currency_code',
'o.is_won',
'o.is_closed',
'o.probability',
'o.forecast_category',
])
->from('opportunities', 'o')
->join('users', 'users.id', '=', 'o.user_id')
->join('groups', 'groups.id', '=', 'users.group_id')
->where('users.team_id', $filter->getTeam()->getId())
->where('o.close_date', '>=', $filter->getStartDate())
->where('o.close_date', '<=', $filter->getEndDate())
->where('o.currency_code', $filter->getCurrency())
->where('o.deleted_at', '=', null)
;
$userUuidList = $filter->getUserUuidList();
if (! empty($userUuidList)) {
$userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);
$opportunities->whereIn('users.uuid', $userUuidList);
}
$groupUuidList = $filter->getGroupUuidList();
if (! empty($groupUuidList)) {
$groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);
$opportunities->whereIn('groups.uuid', $groupUuidList);
}
foreach ($opportunities->cursor() as $row) {
yield new DealData(
(float) $row->value,
$row->close_date,
! empty($row->is_won),
! empty($row->is_closed),
$row->probability ?: 0,
$row->forecast_category ?: '',
);
}
}
public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection
{
return $user->subscriptionSets()
->where(static function (Eloquent\Builder $query): void {
$query
->whereNull('expired_at')
->orWhere('expired_at', '>=', now());
})
->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {
$join
->on('subscription_set_id', '=', 'activity_subscription_sets.id');
$join
->where('followable_type', Models\Activity\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)
->whereIn('followable_id', $opportunityIds);
})
->pluck('followable_id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.25731382,"top":0.019952115,"width":0.03856383,"height":0.025538707},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20157-AJ-report-not-send-notification, menu","depth":5,"bounds":{"left":0.29587767,"top":0.019952115,"width":0.10139628,"height":0.025538707},"help_text":"Git Branch: JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.7972075,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"RequestGenerateAskJiminnyReportJobTest","depth":6,"bounds":{"left":0.8125,"top":0.019952115,"width":0.10305851,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.60106385,"top":0.17478053,"width":0.007978723,"height":0.015163607},"role_description":"text"},{"role":"AXStaticText","text":"111","depth":4,"bounds":{"left":0.61103725,"top":0.17478053,"width":0.010638298,"height":0.015163607},"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.6236702,"top":0.17478053,"width":0.007978723,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.63331115,"top":0.17318435,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.640625,"top":0.17318435,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Console\\Command;\nuse InvalidArgumentException;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateAskJiminnyReportJob;\nuse Jiminny\\Jobs\\AutomatedReports\\SendReportMailJob;\nuse Jiminny\\Jobs\\JobDispatcherInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\n\n/**\n * Class JiminnyDebugCommand\n *\n * @package Jiminny\\Console\\Commands\n */\nclass JiminnyDebugCommand extends Command\n{\n public const string FREQUENCY_DAILY = 'daily';\n public const string FREQUENCY_WEEKLY = 'weekly';\n public const string FREQUENCY_MONTHLY = 'monthly';\n public const string FREQUENCY_QUARTERLY = 'quarterly';\n public const string FREQUENCY_ONE_OFF = 'one_off';\n protected $signature = 'jiminny:debug';\n\n public function handle(JobDispatcherInterface $jobDispatcher, AutomatedReportsService $automatedReportsService): void\n {\n \n \n \n exit(1);\n \n $now = Carbon::now()->subDay(5);\n $from = $now->copy()->previousWeekday()->startOfDay();\n $to = $now->copy()->previousWeekday()->endOfDay();\n\n $this->info(\"Now: {$now->toDateTimeString()}\");\n $this->info(\"From: {$from->toDateTimeString()}\");\n $this->info(\"To: {$to->toDateTimeString()}\");\n\n exit(1);\n\n $report = AutomatedReport::find(71);\n\n $job = new RequestGenerateAskJiminnyReportJob($report->getUuid());\n $jobDispatcher->dispatch($job);\n\n exit(1);\n\n\n // $this->formatDate($jobDispatcher);\n // $this->sendMail($jobDispatcher, $automatedReportsService);\n // $this->crmService();\n\n $this->getPayload($automatedReportsService);\n\n exit(1);\n }\n\n\n\n private function crmService()\n {\n $activity = Activity::find(418141);\n\n $team = Team::find(19);\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n $crmService = $crmResolver->prepareCrmService();\n\n $crmService->createTranscriptNotes($activity);\n }\n\n private function sendMail(JobDispatcherInterface $jobDispatcher, AutomatedReportsService $automatedReportsService)\n {\n $reportUuid = '';\n // $report = $automatedReportsService->getReportResult($reportUuid);\n $report = AutomatedReportResult::find(275);\n $validRecipients = $automatedReportsService->getValidRecipientUsers(\n $report->getReport(),\n includeJiminny: true,\n );\n\n $recipient = $validRecipients[0];\n\n $fileName = $automatedReportsService->getReportFileName($report);\n $typeName = $report->getReport()->getCustomName()\n ?? $automatedReportsService->getReportTypeName($report);\n $teamsName = $automatedReportsService->getReportTeamsName($report);\n $periodName = $automatedReportsService->getReportPeriodName($report);\n $s3Path = $automatedReportsService->getMediaPath($report);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$fileName ' . PHP_EOL . print_r($fileName, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$typeName ' . PHP_EOL . print_r($typeName, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$teamsName ' . PHP_EOL . print_r($teamsName, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$periodName ' . PHP_EOL . print_r($periodName, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$s3Path ' . PHP_EOL . print_r($s3Path, true));\n\n $jobDispatcher->dispatch(\n new SendReportMailJob(\n reportUuid: $report->getUuid(),\n s3Path: $s3Path,\n recipientEmail: $recipient['email'],\n recipientName: $recipient['name'] ?? null,\n fileName: $fileName,\n typeName: $typeName,\n teamsName: $teamsName,\n periodName: $periodName,\n isAskJiminny: true,\n )\n );\n\n exit(1);\n }\n\n private function formatDate(JobDispatcherInterface $jobDispatcher): void\n {\n $customName = 'Custom report name';\n // $frequency = self::FREQUENCY_DAILY;\n // $frequency = self::FREQUENCY_WEEKLY;\n $frequency = self::FREQUENCY_MONTHLY;\n // $frequency = self::FREQUENCY_QUARTERLY;\n // $frequency = self::FREQUENCY_ONE_OFF;\n $period = $this->calculateFromAndToDatePeriod($frequency);\n $from = $period['fromDate'];\n $to = $period['toDate'];\n $periodName = $this->formatReportPeriodName($frequency, $from, $to);\n $filenameSuffix = null;\n\n if ($customName) {\n if ($filenameSuffix) {\n $customName .= \" {$filenameSuffix}\";\n }\n\n $result = $this->sanitizeFileName(\"{$customName} - {$periodName}\");\n }\n\n $this->info($result);\n }\n\n public function calculateFromAndToDatePeriod(\n string $frequency,\n ?Carbon $fromDate = null,\n ?Carbon $toDate = null\n ): array {\n if ($frequency === self::FREQUENCY_ONE_OFF) {\n return [\n 'fromDate' => $fromDate,\n 'toDate' => $toDate,\n ];\n }\n\n $now = Carbon::now();\n\n return match ($frequency) {\n self::FREQUENCY_DAILY => [\n 'fromDate' => $now->copy()->subDay()->startOfDay(),\n 'toDate' => $now->copy()->subDay()->endOfDay(),\n ],\n self::FREQUENCY_WEEKLY => [\n 'fromDate' => $now->copy()->subWeeks(1)->startOfDay(),\n 'toDate' => $now->copy()->subDay()->endOfDay(),\n ],\n self::FREQUENCY_MONTHLY => [\n 'fromDate' => $now->copy()->subMonths(1)->startOfDay(),\n 'toDate' => $now->copy()->subDay()->endOfDay(),\n ],\n self::FREQUENCY_QUARTERLY => [\n 'fromDate' => $now->copy()->subMonths(3)->startOfDay(),\n 'toDate' => $now->copy()->subDay()->endOfDay(),\n ],\n default => throw new InvalidArgumentException(\"Unsupported frequency: {$frequency}\"),\n };\n }\n\n private function formatReportPeriodName(string $frequency, Carbon $from, Carbon $to): string\n {\n $fromYear = $from->format('Y');\n $toYear = $to->format('Y');\n $differentYears = $fromYear !== $toYear;\n\n switch ($frequency) {\n case self::FREQUENCY_DAILY:\n return $from->format('j M Y');\n\n case self::FREQUENCY_QUARTERLY:\n // 'Jan-Mar 2025' or 'Nov 2024-Jan 2025' if years differ\n $startMonth = $from->format('M');\n $endMonth = $to->copy()->subMonth();\n $endMonthName = $endMonth->format('M');\n $endMonthYear = $endMonth->format('Y');\n\n if ($differentYears) {\n return \"{$startMonth} {$fromYear} - {$endMonthName} {$endMonthYear}\";\n }\n\n return \"{$startMonth} - {$endMonthName} {$toYear}\";\n\n case self::FREQUENCY_MONTHLY:\n // 'May 2025' - monthly reports are always within the same year\n return $from->format('M Y');\n\n case self::FREQUENCY_WEEKLY:\n // '4 - 8 Aug 2025', '27 Oct - 3 Nov 2025', or '28 Dec 2024 - 3 Jan 2025' if years differ\n $startDay = $from->format('j');\n $endDay = $to->format('j');\n $startMonth = $from->format('M');\n $endMonth = $to->format('M');\n\n if ($differentYears) {\n return \"{$startDay} {$startMonth} {$fromYear} - {$endDay} {$endMonth} {$toYear}\";\n }\n\n if ($startMonth !== $endMonth) {\n return \"{$startDay} {$startMonth} - {$endDay} {$endMonth} {$toYear}\";\n }\n\n return \"{$startDay} - {$endDay} {$endMonth} {$toYear}\";\n\n case self::FREQUENCY_ONE_OFF:\n // '2 May-31 May 2025' or '15 Dec 2024-15 Jan 2025' if years differ\n $startDay = $from->format('j');\n $startMonth = $from->format('M');\n $endDay = $to->format('j');\n $endMonth = $to->format('M');\n\n // If same month and year, use a format like '2-31 May 2025'\n if ($startMonth === $endMonth && ! $differentYears) {\n return \"{$startDay} - {$endDay} {$startMonth} {$toYear}\";\n }\n\n // If different years, include both years\n if ($differentYears) {\n return \"{$startDay} {$startMonth} {$fromYear} - {$endDay} {$endMonth} {$toYear}\";\n }\n\n // Same year but different months\n return \"{$startDay} {$startMonth} - {$endDay} {$endMonth} {$toYear}\";\n\n default:\n // Default format for unknown frequencies\n return $from->format('j M Y') . ' - ' . $to->format('j M Y');\n }\n }\n\n public function sanitizeFileName(string $fileName): string\n {\n return str_replace(['/', '\\\\'], '-', $fileName);\n }\n\n private function getPayload(AutomatedReportsService $automatedReportsService)\n {\n $reportResult = AutomatedReportResult::find(269);\n $automatedReport = $reportResult->getReport();\n $activityIds = [1,2,3];\n $payload = $automatedReportsService->getAskJiminnyGenerateReportPayload(\n automatedReport: $automatedReport,\n reportResult: $reportResult,\n activityIds: $activityIds,\n );\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$payload ' . PHP_EOL . print_r($payload, true));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Console\\Command;\nuse InvalidArgumentException;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateAskJiminnyReportJob;\nuse Jiminny\\Jobs\\AutomatedReports\\SendReportMailJob;\nuse Jiminny\\Jobs\\JobDispatcherInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\n\n/**\n * Class JiminnyDebugCommand\n *\n * @package Jiminny\\Console\\Commands\n */\nclass JiminnyDebugCommand extends Command\n{\n public const string FREQUENCY_DAILY = 'daily';\n public const string FREQUENCY_WEEKLY = 'weekly';\n public const string FREQUENCY_MONTHLY = 'monthly';\n public const string FREQUENCY_QUARTERLY = 'quarterly';\n public const string FREQUENCY_ONE_OFF = 'one_off';\n protected $signature = 'jiminny:debug';\n\n public function handle(JobDispatcherInterface $jobDispatcher, AutomatedReportsService $automatedReportsService): void\n {\n \n \n \n exit(1);\n \n $now = Carbon::now()->subDay(5);\n $from = $now->copy()->previousWeekday()->startOfDay();\n $to = $now->copy()->previousWeekday()->endOfDay();\n\n $this->info(\"Now: {$now->toDateTimeString()}\");\n $this->info(\"From: {$from->toDateTimeString()}\");\n $this->info(\"To: {$to->toDateTimeString()}\");\n\n exit(1);\n\n $report = AutomatedReport::find(71);\n\n $job = new RequestGenerateAskJiminnyReportJob($report->getUuid());\n $jobDispatcher->dispatch($job);\n\n exit(1);\n\n\n // $this->formatDate($jobDispatcher);\n // $this->sendMail($jobDispatcher, $automatedReportsService);\n // $this->crmService();\n\n $this->getPayload($automatedReportsService);\n\n exit(1);\n }\n\n\n\n private function crmService()\n {\n $activity = Activity::find(418141);\n\n $team = Team::find(19);\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n $crmService = $crmResolver->prepareCrmService();\n\n $crmService->createTranscriptNotes($activity);\n }\n\n private function sendMail(JobDispatcherInterface $jobDispatcher, AutomatedReportsService $automatedReportsService)\n {\n $reportUuid = '';\n // $report = $automatedReportsService->getReportResult($reportUuid);\n $report = AutomatedReportResult::find(275);\n $validRecipients = $automatedReportsService->getValidRecipientUsers(\n $report->getReport(),\n includeJiminny: true,\n );\n\n $recipient = $validRecipients[0];\n\n $fileName = $automatedReportsService->getReportFileName($report);\n $typeName = $report->getReport()->getCustomName()\n ?? $automatedReportsService->getReportTypeName($report);\n $teamsName = $automatedReportsService->getReportTeamsName($report);\n $periodName = $automatedReportsService->getReportPeriodName($report);\n $s3Path = $automatedReportsService->getMediaPath($report);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$fileName ' . PHP_EOL . print_r($fileName, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$typeName ' . PHP_EOL . print_r($typeName, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$teamsName ' . PHP_EOL . print_r($teamsName, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$periodName ' . PHP_EOL . print_r($periodName, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$s3Path ' . PHP_EOL . print_r($s3Path, true));\n\n $jobDispatcher->dispatch(\n new SendReportMailJob(\n reportUuid: $report->getUuid(),\n s3Path: $s3Path,\n recipientEmail: $recipient['email'],\n recipientName: $recipient['name'] ?? null,\n fileName: $fileName,\n typeName: $typeName,\n teamsName: $teamsName,\n periodName: $periodName,\n isAskJiminny: true,\n )\n );\n\n exit(1);\n }\n\n private function formatDate(JobDispatcherInterface $jobDispatcher): void\n {\n $customName = 'Custom report name';\n // $frequency = self::FREQUENCY_DAILY;\n // $frequency = self::FREQUENCY_WEEKLY;\n $frequency = self::FREQUENCY_MONTHLY;\n // $frequency = self::FREQUENCY_QUARTERLY;\n // $frequency = self::FREQUENCY_ONE_OFF;\n $period = $this->calculateFromAndToDatePeriod($frequency);\n $from = $period['fromDate'];\n $to = $period['toDate'];\n $periodName = $this->formatReportPeriodName($frequency, $from, $to);\n $filenameSuffix = null;\n\n if ($customName) {\n if ($filenameSuffix) {\n $customName .= \" {$filenameSuffix}\";\n }\n\n $result = $this->sanitizeFileName(\"{$customName} - {$periodName}\");\n }\n\n $this->info($result);\n }\n\n public function calculateFromAndToDatePeriod(\n string $frequency,\n ?Carbon $fromDate = null,\n ?Carbon $toDate = null\n ): array {\n if ($frequency === self::FREQUENCY_ONE_OFF) {\n return [\n 'fromDate' => $fromDate,\n 'toDate' => $toDate,\n ];\n }\n\n $now = Carbon::now();\n\n return match ($frequency) {\n self::FREQUENCY_DAILY => [\n 'fromDate' => $now->copy()->subDay()->startOfDay(),\n 'toDate' => $now->copy()->subDay()->endOfDay(),\n ],\n self::FREQUENCY_WEEKLY => [\n 'fromDate' => $now->copy()->subWeeks(1)->startOfDay(),\n 'toDate' => $now->copy()->subDay()->endOfDay(),\n ],\n self::FREQUENCY_MONTHLY => [\n 'fromDate' => $now->copy()->subMonths(1)->startOfDay(),\n 'toDate' => $now->copy()->subDay()->endOfDay(),\n ],\n self::FREQUENCY_QUARTERLY => [\n 'fromDate' => $now->copy()->subMonths(3)->startOfDay(),\n 'toDate' => $now->copy()->subDay()->endOfDay(),\n ],\n default => throw new InvalidArgumentException(\"Unsupported frequency: {$frequency}\"),\n };\n }\n\n private function formatReportPeriodName(string $frequency, Carbon $from, Carbon $to): string\n {\n $fromYear = $from->format('Y');\n $toYear = $to->format('Y');\n $differentYears = $fromYear !== $toYear;\n\n switch ($frequency) {\n case self::FREQUENCY_DAILY:\n return $from->format('j M Y');\n\n case self::FREQUENCY_QUARTERLY:\n // 'Jan-Mar 2025' or 'Nov 2024-Jan 2025' if years differ\n $startMonth = $from->format('M');\n $endMonth = $to->copy()->subMonth();\n $endMonthName = $endMonth->format('M');\n $endMonthYear = $endMonth->format('Y');\n\n if ($differentYears) {\n return \"{$startMonth} {$fromYear} - {$endMonthName} {$endMonthYear}\";\n }\n\n return \"{$startMonth} - {$endMonthName} {$toYear}\";\n\n case self::FREQUENCY_MONTHLY:\n // 'May 2025' - monthly reports are always within the same year\n return $from->format('M Y');\n\n case self::FREQUENCY_WEEKLY:\n // '4 - 8 Aug 2025', '27 Oct - 3 Nov 2025', or '28 Dec 2024 - 3 Jan 2025' if years differ\n $startDay = $from->format('j');\n $endDay = $to->format('j');\n $startMonth = $from->format('M');\n $endMonth = $to->format('M');\n\n if ($differentYears) {\n return \"{$startDay} {$startMonth} {$fromYear} - {$endDay} {$endMonth} {$toYear}\";\n }\n\n if ($startMonth !== $endMonth) {\n return \"{$startDay} {$startMonth} - {$endDay} {$endMonth} {$toYear}\";\n }\n\n return \"{$startDay} - {$endDay} {$endMonth} {$toYear}\";\n\n case self::FREQUENCY_ONE_OFF:\n // '2 May-31 May 2025' or '15 Dec 2024-15 Jan 2025' if years differ\n $startDay = $from->format('j');\n $startMonth = $from->format('M');\n $endDay = $to->format('j');\n $endMonth = $to->format('M');\n\n // If same month and year, use a format like '2-31 May 2025'\n if ($startMonth === $endMonth && ! $differentYears) {\n return \"{$startDay} - {$endDay} {$startMonth} {$toYear}\";\n }\n\n // If different years, include both years\n if ($differentYears) {\n return \"{$startDay} {$startMonth} {$fromYear} - {$endDay} {$endMonth} {$toYear}\";\n }\n\n // Same year but different months\n return \"{$startDay} {$startMonth} - {$endDay} {$endMonth} {$toYear}\";\n\n default:\n // Default format for unknown frequencies\n return $from->format('j M Y') . ' - ' . $to->format('j M Y');\n }\n }\n\n public function sanitizeFileName(string $fileName): string\n {\n return str_replace(['/', '\\\\'], '-', $fileName);\n }\n\n private function getPayload(AutomatedReportsService $automatedReportsService)\n {\n $reportResult = AutomatedReportResult::find(269);\n $automatedReport = $reportResult->getReport();\n $activityIds = [1,2,3];\n $payload = $automatedReportsService->getAskJiminnyGenerateReportPayload(\n automatedReport: $automatedReport,\n reportResult: $reportResult,\n activityIds: $activityIds,\n );\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$payload ' . PHP_EOL . print_r($payload, true));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"36","depth":4,"bounds":{"left":0.69980055,"top":0.2490024,"width":0.010305851,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7117686,"top":0.24740623,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7190825,"top":0.24740623,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\DealInsights;\n\nuse Doctrine\\DBAL\\Connection;\nuse Generator;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealData;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealsFilter;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\QueryBuilder;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\Visitor\\QueryBuilderVisitorInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Services\\Crm\\IntegrationApp\\DTO\\Utils\\UrlGeneratorInterface;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Eloquent;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass DealsRepository implements DealsRepositoryInterface\n{\n private Connection $connection;\n\n private ProviderRegistry $providerRegistry;\n\n /**\n * @var QueryBuilderVisitorInterface[]\n */\n private array $visitors = [];\n\n /**\n * @param QueryBuilderVisitorInterface[] $visitors\n */\n public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])\n {\n $this->connection = $connection;\n $this->providerRegistry = $crmProviderRegistry;\n\n foreach ($visitors as $visitor) {\n $this->visitors[$visitor->getIdentifier()] = $visitor;\n }\n }\n\n public function getDeals(CriteriaInterface $criteria): array\n {\n $context = $criteria->getContext();\n $team = $context->getTeam();\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n\n $this->visit($qb, $criteria);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getDeal(Team $team, int $id): array\n {\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n $qb->andWhere('opp.id = :id')->setParameter('id', $id);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')\n ->from('crm_fields', 'f')\n ->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')\n ->where('f.crm_configuration_id = :crm')\n ->andWhere('f.object_type = :type')\n ->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')\n ->orderBy('fd.object_id', 'ASC')\n ->addOrderBy('fd.updated_at', 'ASC')\n\n ->setParameter('type', Field::OBJECT_OPPORTUNITY)\n ->setParameter('crm', $crmId)\n ;\n\n if (! empty($crmFields)) {\n $fields = array_map(fn ($value): string => '\"' . $value . '\"', $crmFields);\n $qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');\n }\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAssociative();\n }\n\n public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('COALESCE(opp.currency_code, \"' . $defaultCurrency . '\") AS currency')\n ->addSelect('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ->groupBy('currency')\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getDealActivities(CriteriaInterface $criteria): array\n {\n $qb = Activity::with(['participants', 'user'])\n ->where('opportunity_id', $criteria->getOpportunityId())\n ->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())\n ->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())\n ->orderBy($criteria->getSortBy(), $criteria->getSortDirection())\n ;\n\n // Should we filter activities by criteria? It's intended to filter deals.\n\n return $qb->get()->all();\n }\n\n public function getStages(CriteriaInterface $criteria): array\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('id', 'label', 'sequence')\n ->from('stages', 's')\n ->where('crm_configuration_id = :crm_configuration_id')\n ->andWhere('type = :type')\n ->orderBy('sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())\n ->setParameter('type', Stage::TYPE_OPPORTUNITY);\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $result[$row['id']] = [\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n public function getConfigurationStages(Configuration $configuration): Collection\n {\n return $configuration\n ->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get();\n }\n\n public function getPipelineData(Configuration $crm): array\n {\n $qb = new QueryBuilder($this->connection);\n $provider = $crm->provider;\n\n $qb\n ->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')\n ->from('stages', 's')\n ->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')\n ->where('s.crm_configuration_id = :crm_configuration_id')\n ->andWhere('s.type = :type')\n ->orderBy('bps.business_process_id', 'ASC')\n ->addOrderBy('s.sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $crm->id)\n ->setParameter('type', Stage::TYPE_OPPORTUNITY)\n ;\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];\n $result[$row['pipeline_id']][] = [\n 'value' => $value,\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n private function createQueryBuilder(string $realm): QueryBuilder\n {\n return (new QueryBuilder($this->connection))\n ->setRealm($realm)\n ->from('opportunities', 'opp')\n ->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')\n ->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')\n ->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')\n ;\n }\n\n /**\n * Applies all applicable visitors and returns the IDs of the executed ones\n *\n * @return string[]\n */\n private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array\n {\n $queryVisitors = [];\n\n foreach ($this->visitors as $visitor) {\n if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {\n $visitor->visit($queryBuilder, $criteria);\n\n $queryVisitors[] = $visitor->getIdentifier();\n }\n }\n\n return $queryVisitors;\n }\n\n private function hydrateStages(array $deals): array\n {\n foreach ($this->fetchStages(array_keys($deals)) as $stage) {\n $oppId = (int) $stage['opportunity_id'];\n\n if (! isset($deals[$oppId])) {\n continue; // or throw??!\n }\n\n $deals[$oppId]['stages'][] = [\n 'id' => $stage['stage_id'],\n 'name' => $stage['label'],\n 'enteredAt' => $stage['created_at'],\n ];\n }\n\n return $deals;\n }\n\n /**\n * @param int[] $dealIds\n */\n private function fetchStages(array $dealIds): array\n {\n if (empty($dealIds)) {\n return [];\n }\n\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')\n ->from('opportunity_stages', 'os')\n ->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')\n ->where($qb->expr()->in('os.opportunity_id', $dealIds))\n ->orderBy('os.opportunity_id', 'ASC')\n ->addOrderBy('s.created_at', 'ASC')\n ;\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array\n {\n $result = [];\n\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $data = [\n 'uuid' => RequiresUUID::toNormal($row['uuid']),\n 'name' => $row['name'],\n 'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),\n 'account' => [\n 'name' => $row['acc_name'],\n 'url' => $crmService->generateProviderUrl(\n providerId: $row['acc_provider_id'],\n objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'\n ),\n ],\n 'owner' => null,\n 'rawValue' => [\n 'amount' => (float) $row['value'],\n 'currency' => $row['currency_code'],\n ],\n 'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),\n 'openDate' => $row['remotely_created_at'] ?? null,\n 'closeDate' => $row['close_date'] ?? null,\n 'stages' => [],\n 'currentPipelineId' => $row['pipeline_id'],\n 'currentStage' => [\n 'id' => $row['stage_id'],\n 'enteredAt' => $row['stage_updated_at'],\n ],\n 'currentStageUpdatedAt' => $row['stage_updated_at'],\n 'isClosed' => (bool) $row['is_closed'],\n 'isWon' => (bool) $row['is_won'],\n ];\n\n if (isset($row['owner_uuid'])) {\n $data['owner'] = [\n 'uuid' => RequiresUUID::toNormal($row['owner_uuid']),\n 'name' => $row['owner_name'],\n 'photoUrl' => $row['owner_photo'] === null\n ? null\n : client_cdn($row['owner_photo'], $team),\n 'id' => $row['owner_id'],\n 'job' => $row['owner_job'],\n ];\n }\n\n $result[(int) $row['opp_id']] = $data;\n }\n\n return $this->hydrateStages($result);\n }\n\n private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder\n {\n $qb = clone $queryBuilder;\n $qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');\n\n $qb\n ->select(...[\n 'opp.id as opp_id',\n 'opp.uuid',\n 'opp.name',\n 'opp.value',\n 'opp.currency_code',\n 'opp.close_date',\n 'opp.remotely_created_at',\n 'opp.is_closed',\n 'opp.is_won',\n ])\n ->addSelect(...[\n 'usr.uuid as owner_uuid',\n 'usr.name AS owner_name',\n 'usr.photo_path as owner_photo',\n 'usr.id AS owner_id',\n 'jt.name as owner_job',\n ])\n ->addSelect('opp.stage_id', 'opp.stage_updated_at')\n ->addSelect(...[\n 'acc.name AS acc_name',\n 'acc.is_internal as acc_is_internal',\n 'opp.stage_updated_at',\n 'acc.crm_provider_id AS acc_provider_id',\n 'opp.crm_provider_id AS opp_provider_id',\n ])\n ->addSelect('rt.business_process_id AS pipeline_id')\n\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'));\n\n return $qb;\n }\n\n /**\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws SocialAccountTokenInvalidException\n */\n private function getCrmService(Team $team): ServiceInterface\n {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n $crmService->setConfiguration($team->crm);\n if ($crmService instanceof UrlGeneratorInterface) {\n $crmService->setCrmUrlGenerator($team->crm);\n }\n\n return $crmService;\n }\n\n /**\n *\n * @return Generator<DealData>\n */\n public function getForecastData(DealsFilter $filter): Generator\n {\n $opportunities = DB::query()\n ->select([\n 'o.value',\n 'o.close_date',\n 'o.currency_code',\n 'o.is_won',\n 'o.is_closed',\n 'o.probability',\n 'o.forecast_category',\n ])\n ->from('opportunities', 'o')\n ->join('users', 'users.id', '=', 'o.user_id')\n ->join('groups', 'groups.id', '=', 'users.group_id')\n ->where('users.team_id', $filter->getTeam()->getId())\n ->where('o.close_date', '>=', $filter->getStartDate())\n ->where('o.close_date', '<=', $filter->getEndDate())\n ->where('o.currency_code', $filter->getCurrency())\n ->where('o.deleted_at', '=', null)\n ;\n\n $userUuidList = $filter->getUserUuidList();\n if (! empty($userUuidList)) {\n $userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);\n\n $opportunities->whereIn('users.uuid', $userUuidList);\n }\n\n $groupUuidList = $filter->getGroupUuidList();\n if (! empty($groupUuidList)) {\n $groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);\n\n $opportunities->whereIn('groups.uuid', $groupUuidList);\n }\n\n foreach ($opportunities->cursor() as $row) {\n yield new DealData(\n (float) $row->value,\n $row->close_date,\n ! empty($row->is_won),\n ! empty($row->is_closed),\n $row->probability ?: 0,\n $row->forecast_category ?: '',\n );\n }\n }\n\n public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection\n {\n return $user->subscriptionSets()\n ->where(static function (Eloquent\\Builder $query): void {\n $query\n ->whereNull('expired_at')\n ->orWhere('expired_at', '>=', now());\n })\n ->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n $join\n ->where('followable_type', Models\\Activity\\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)\n ->whereIn('followable_id', $opportunityIds);\n })\n ->pluck('followable_id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\DealInsights;\n\nuse Doctrine\\DBAL\\Connection;\nuse Generator;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealData;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealsFilter;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\QueryBuilder;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\Visitor\\QueryBuilderVisitorInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Services\\Crm\\IntegrationApp\\DTO\\Utils\\UrlGeneratorInterface;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Eloquent;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass DealsRepository implements DealsRepositoryInterface\n{\n private Connection $connection;\n\n private ProviderRegistry $providerRegistry;\n\n /**\n * @var QueryBuilderVisitorInterface[]\n */\n private array $visitors = [];\n\n /**\n * @param QueryBuilderVisitorInterface[] $visitors\n */\n public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])\n {\n $this->connection = $connection;\n $this->providerRegistry = $crmProviderRegistry;\n\n foreach ($visitors as $visitor) {\n $this->visitors[$visitor->getIdentifier()] = $visitor;\n }\n }\n\n public function getDeals(CriteriaInterface $criteria): array\n {\n $context = $criteria->getContext();\n $team = $context->getTeam();\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n\n $this->visit($qb, $criteria);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getDeal(Team $team, int $id): array\n {\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n $qb->andWhere('opp.id = :id')->setParameter('id', $id);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')\n ->from('crm_fields', 'f')\n ->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')\n ->where('f.crm_configuration_id = :crm')\n ->andWhere('f.object_type = :type')\n ->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')\n ->orderBy('fd.object_id', 'ASC')\n ->addOrderBy('fd.updated_at', 'ASC')\n\n ->setParameter('type', Field::OBJECT_OPPORTUNITY)\n ->setParameter('crm', $crmId)\n ;\n\n if (! empty($crmFields)) {\n $fields = array_map(fn ($value): string => '\"' . $value . '\"', $crmFields);\n $qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');\n }\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAssociative();\n }\n\n public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('COALESCE(opp.currency_code, \"' . $defaultCurrency . '\") AS currency')\n ->addSelect('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ->groupBy('currency')\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getDealActivities(CriteriaInterface $criteria): array\n {\n $qb = Activity::with(['participants', 'user'])\n ->where('opportunity_id', $criteria->getOpportunityId())\n ->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())\n ->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())\n ->orderBy($criteria->getSortBy(), $criteria->getSortDirection())\n ;\n\n // Should we filter activities by criteria? It's intended to filter deals.\n\n return $qb->get()->all();\n }\n\n public function getStages(CriteriaInterface $criteria): array\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('id', 'label', 'sequence')\n ->from('stages', 's')\n ->where('crm_configuration_id = :crm_configuration_id')\n ->andWhere('type = :type')\n ->orderBy('sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())\n ->setParameter('type', Stage::TYPE_OPPORTUNITY);\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $result[$row['id']] = [\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n public function getConfigurationStages(Configuration $configuration): Collection\n {\n return $configuration\n ->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get();\n }\n\n public function getPipelineData(Configuration $crm): array\n {\n $qb = new QueryBuilder($this->connection);\n $provider = $crm->provider;\n\n $qb\n ->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')\n ->from('stages', 's')\n ->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')\n ->where('s.crm_configuration_id = :crm_configuration_id')\n ->andWhere('s.type = :type')\n ->orderBy('bps.business_process_id', 'ASC')\n ->addOrderBy('s.sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $crm->id)\n ->setParameter('type', Stage::TYPE_OPPORTUNITY)\n ;\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];\n $result[$row['pipeline_id']][] = [\n 'value' => $value,\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n private function createQueryBuilder(string $realm): QueryBuilder\n {\n return (new QueryBuilder($this->connection))\n ->setRealm($realm)\n ->from('opportunities', 'opp')\n ->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')\n ->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')\n ->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')\n ;\n }\n\n /**\n * Applies all applicable visitors and returns the IDs of the executed ones\n *\n * @return string[]\n */\n private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array\n {\n $queryVisitors = [];\n\n foreach ($this->visitors as $visitor) {\n if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {\n $visitor->visit($queryBuilder, $criteria);\n\n $queryVisitors[] = $visitor->getIdentifier();\n }\n }\n\n return $queryVisitors;\n }\n\n private function hydrateStages(array $deals): array\n {\n foreach ($this->fetchStages(array_keys($deals)) as $stage) {\n $oppId = (int) $stage['opportunity_id'];\n\n if (! isset($deals[$oppId])) {\n continue; // or throw??!\n }\n\n $deals[$oppId]['stages'][] = [\n 'id' => $stage['stage_id'],\n 'name' => $stage['label'],\n 'enteredAt' => $stage['created_at'],\n ];\n }\n\n return $deals;\n }\n\n /**\n * @param int[] $dealIds\n */\n private function fetchStages(array $dealIds): array\n {\n if (empty($dealIds)) {\n return [];\n }\n\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')\n ->from('opportunity_stages', 'os')\n ->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')\n ->where($qb->expr()->in('os.opportunity_id', $dealIds))\n ->orderBy('os.opportunity_id', 'ASC')\n ->addOrderBy('s.created_at', 'ASC')\n ;\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array\n {\n $result = [];\n\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $data = [\n 'uuid' => RequiresUUID::toNormal($row['uuid']),\n 'name' => $row['name'],\n 'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),\n 'account' => [\n 'name' => $row['acc_name'],\n 'url' => $crmService->generateProviderUrl(\n providerId: $row['acc_provider_id'],\n objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'\n ),\n ],\n 'owner' => null,\n 'rawValue' => [\n 'amount' => (float) $row['value'],\n 'currency' => $row['currency_code'],\n ],\n 'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),\n 'openDate' => $row['remotely_created_at'] ?? null,\n 'closeDate' => $row['close_date'] ?? null,\n 'stages' => [],\n 'currentPipelineId' => $row['pipeline_id'],\n 'currentStage' => [\n 'id' => $row['stage_id'],\n 'enteredAt' => $row['stage_updated_at'],\n ],\n 'currentStageUpdatedAt' => $row['stage_updated_at'],\n 'isClosed' => (bool) $row['is_closed'],\n 'isWon' => (bool) $row['is_won'],\n ];\n\n if (isset($row['owner_uuid'])) {\n $data['owner'] = [\n 'uuid' => RequiresUUID::toNormal($row['owner_uuid']),\n 'name' => $row['owner_name'],\n 'photoUrl' => $row['owner_photo'] === null\n ? null\n : client_cdn($row['owner_photo'], $team),\n 'id' => $row['owner_id'],\n 'job' => $row['owner_job'],\n ];\n }\n\n $result[(int) $row['opp_id']] = $data;\n }\n\n return $this->hydrateStages($result);\n }\n\n private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder\n {\n $qb = clone $queryBuilder;\n $qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');\n\n $qb\n ->select(...[\n 'opp.id as opp_id',\n 'opp.uuid',\n 'opp.name',\n 'opp.value',\n 'opp.currency_code',\n 'opp.close_date',\n 'opp.remotely_created_at',\n 'opp.is_closed',\n 'opp.is_won',\n ])\n ->addSelect(...[\n 'usr.uuid as owner_uuid',\n 'usr.name AS owner_name',\n 'usr.photo_path as owner_photo',\n 'usr.id AS owner_id',\n 'jt.name as owner_job',\n ])\n ->addSelect('opp.stage_id', 'opp.stage_updated_at')\n ->addSelect(...[\n 'acc.name AS acc_name',\n 'acc.is_internal as acc_is_internal',\n 'opp.stage_updated_at',\n 'acc.crm_provider_id AS acc_provider_id',\n 'opp.crm_provider_id AS opp_provider_id',\n ])\n ->addSelect('rt.business_process_id AS pipeline_id')\n\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'));\n\n return $qb;\n }\n\n /**\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws SocialAccountTokenInvalidException\n */\n private function getCrmService(Team $team): ServiceInterface\n {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n $crmService->setConfiguration($team->crm);\n if ($crmService instanceof UrlGeneratorInterface) {\n $crmService->setCrmUrlGenerator($team->crm);\n }\n\n return $crmService;\n }\n\n /**\n *\n * @return Generator<DealData>\n */\n public function getForecastData(DealsFilter $filter): Generator\n {\n $opportunities = DB::query()\n ->select([\n 'o.value',\n 'o.close_date',\n 'o.currency_code',\n 'o.is_won',\n 'o.is_closed',\n 'o.probability',\n 'o.forecast_category',\n ])\n ->from('opportunities', 'o')\n ->join('users', 'users.id', '=', 'o.user_id')\n ->join('groups', 'groups.id', '=', 'users.group_id')\n ->where('users.team_id', $filter->getTeam()->getId())\n ->where('o.close_date', '>=', $filter->getStartDate())\n ->where('o.close_date', '<=', $filter->getEndDate())\n ->where('o.currency_code', $filter->getCurrency())\n ->where('o.deleted_at', '=', null)\n ;\n\n $userUuidList = $filter->getUserUuidList();\n if (! empty($userUuidList)) {\n $userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);\n\n $opportunities->whereIn('users.uuid', $userUuidList);\n }\n\n $groupUuidList = $filter->getGroupUuidList();\n if (! empty($groupUuidList)) {\n $groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);\n\n $opportunities->whereIn('groups.uuid', $groupUuidList);\n }\n\n foreach ($opportunities->cursor() as $row) {\n yield new DealData(\n (float) $row->value,\n $row->close_date,\n ! empty($row->is_won),\n ! empty($row->is_closed),\n $row->probability ?: 0,\n $row->forecast_category ?: '',\n );\n }\n }\n\n public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection\n {\n return $user->subscriptionSets()\n ->where(static function (Eloquent\\Builder $query): void {\n $query\n ->whereNull('expired_at')\n ->orWhere('expired_at', '>=', now());\n })\n ->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n $join\n ->where('followable_type', Models\\Activity\\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)\n ->whereIn('followable_id', $opportunityIds);\n })\n ->pluck('followable_id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.24335106,"top":0.047885075,"width":0.024268618,"height":0.024740623},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
6727517982887829105
|
7071332928453159851
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
111
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands;
use Carbon\Carbon;
use Illuminate\Console\Command;
use InvalidArgumentException;
use Jiminny\Jobs\AutomatedReports\RequestGenerateAskJiminnyReportJob;
use Jiminny\Jobs\AutomatedReports\SendReportMailJob;
use Jiminny\Jobs\JobDispatcherInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
/**
* Class JiminnyDebugCommand
*
* @package Jiminny\Console\Commands
*/
class JiminnyDebugCommand extends Command
{
public const string FREQUENCY_DAILY = 'daily';
public const string FREQUENCY_WEEKLY = 'weekly';
public const string FREQUENCY_MONTHLY = 'monthly';
public const string FREQUENCY_QUARTERLY = 'quarterly';
public const string FREQUENCY_ONE_OFF = 'one_off';
protected $signature = 'jiminny:debug';
public function handle(JobDispatcherInterface $jobDispatcher, AutomatedReportsService $automatedReportsService): void
{
exit(1);
$now = Carbon::now()->subDay(5);
$from = $now->copy()->previousWeekday()->startOfDay();
$to = $now->copy()->previousWeekday()->endOfDay();
$this->info("Now: {$now->toDateTimeString()}");
$this->info("From: {$from->toDateTimeString()}");
$this->info("To: {$to->toDateTimeString()}");
exit(1);
$report = AutomatedReport::find(71);
$job = new RequestGenerateAskJiminnyReportJob($report->getUuid());
$jobDispatcher->dispatch($job);
exit(1);
// $this->formatDate($jobDispatcher);
// $this->sendMail($jobDispatcher, $automatedReportsService);
// $this->crmService();
$this->getPayload($automatedReportsService);
exit(1);
}
private function crmService()
{
$activity = Activity::find(418141);
$team = Team::find(19);
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
$crmService->createTranscriptNotes($activity);
}
private function sendMail(JobDispatcherInterface $jobDispatcher, AutomatedReportsService $automatedReportsService)
{
$reportUuid = '';
// $report = $automatedReportsService->getReportResult($reportUuid);
$report = AutomatedReportResult::find(275);
$validRecipients = $automatedReportsService->getValidRecipientUsers(
$report->getReport(),
includeJiminny: true,
);
$recipient = $validRecipients[0];
$fileName = $automatedReportsService->getReportFileName($report);
$typeName = $report->getReport()->getCustomName()
?? $automatedReportsService->getReportTypeName($report);
$teamsName = $automatedReportsService->getReportTeamsName($report);
$periodName = $automatedReportsService->getReportPeriodName($report);
$s3Path = $automatedReportsService->getMediaPath($report);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$fileName ' . PHP_EOL . print_r($fileName, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$typeName ' . PHP_EOL . print_r($typeName, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$teamsName ' . PHP_EOL . print_r($teamsName, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$periodName ' . PHP_EOL . print_r($periodName, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$s3Path ' . PHP_EOL . print_r($s3Path, true));
$jobDispatcher->dispatch(
new SendReportMailJob(
reportUuid: $report->getUuid(),
s3Path: $s3Path,
recipientEmail: $recipient['email'],
recipientName: $recipient['name'] ?? null,
fileName: $fileName,
typeName: $typeName,
teamsName: $teamsName,
periodName: $periodName,
isAskJiminny: true,
)
);
exit(1);
}
private function formatDate(JobDispatcherInterface $jobDispatcher): void
{
$customName = 'Custom report name';
// $frequency = self::FREQUENCY_DAILY;
// $frequency = self::FREQUENCY_WEEKLY;
$frequency = self::FREQUENCY_MONTHLY;
// $frequency = self::FREQUENCY_QUARTERLY;
// $frequency = self::FREQUENCY_ONE_OFF;
$period = $this->calculateFromAndToDatePeriod($frequency);
$from = $period['fromDate'];
$to = $period['toDate'];
$periodName = $this->formatReportPeriodName($frequency, $from, $to);
$filenameSuffix = null;
if ($customName) {
if ($filenameSuffix) {
$customName .= " {$filenameSuffix}";
}
$result = $this->sanitizeFileName("{$customName} - {$periodName}");
}
$this->info($result);
}
public function calculateFromAndToDatePeriod(
string $frequency,
?Carbon $fromDate = null,
?Carbon $toDate = null
): array {
if ($frequency === self::FREQUENCY_ONE_OFF) {
return [
'fromDate' => $fromDate,
'toDate' => $toDate,
];
}
$now = Carbon::now();
return match ($frequency) {
self::FREQUENCY_DAILY => [
'fromDate' => $now->copy()->subDay()->startOfDay(),
'toDate' => $now->copy()->subDay()->endOfDay(),
],
self::FREQUENCY_WEEKLY => [
'fromDate' => $now->copy()->subWeeks(1)->startOfDay(),
'toDate' => $now->copy()->subDay()->endOfDay(),
],
self::FREQUENCY_MONTHLY => [
'fromDate' => $now->copy()->subMonths(1)->startOfDay(),
'toDate' => $now->copy()->subDay()->endOfDay(),
],
self::FREQUENCY_QUARTERLY => [
'fromDate' => $now->copy()->subMonths(3)->startOfDay(),
'toDate' => $now->copy()->subDay()->endOfDay(),
],
default => throw new InvalidArgumentException("Unsupported frequency: {$frequency}"),
};
}
private function formatReportPeriodName(string $frequency, Carbon $from, Carbon $to): string
{
$fromYear = $from->format('Y');
$toYear = $to->format('Y');
$differentYears = $fromYear !== $toYear;
switch ($frequency) {
case self::FREQUENCY_DAILY:
return $from->format('j M Y');
case self::FREQUENCY_QUARTERLY:
// 'Jan-Mar 2025' or 'Nov 2024-Jan 2025' if years differ
$startMonth = $from->format('M');
$endMonth = $to->copy()->subMonth();
$endMonthName = $endMonth->format('M');
$endMonthYear = $endMonth->format('Y');
if ($differentYears) {
return "{$startMonth} {$fromYear} - {$endMonthName} {$endMonthYear}";
}
return "{$startMonth} - {$endMonthName} {$toYear}";
case self::FREQUENCY_MONTHLY:
// 'May 2025' - monthly reports are always within the same year
return $from->format('M Y');
case self::FREQUENCY_WEEKLY:
// '4 - 8 Aug 2025', '27 Oct - 3 Nov 2025', or '28 Dec 2024 - 3 Jan 2025' if years differ
$startDay = $from->format('j');
$endDay = $to->format('j');
$startMonth = $from->format('M');
$endMonth = $to->format('M');
if ($differentYears) {
return "{$startDay} {$startMonth} {$fromYear} - {$endDay} {$endMonth} {$toYear}";
}
if ($startMonth !== $endMonth) {
return "{$startDay} {$startMonth} - {$endDay} {$endMonth} {$toYear}";
}
return "{$startDay} - {$endDay} {$endMonth} {$toYear}";
case self::FREQUENCY_ONE_OFF:
// '2 May-31 May 2025' or '15 Dec 2024-15 Jan 2025' if years differ
$startDay = $from->format('j');
$startMonth = $from->format('M');
$endDay = $to->format('j');
$endMonth = $to->format('M');
// If same month and year, use a format like '2-31 May 2025'
if ($startMonth === $endMonth && ! $differentYears) {
return "{$startDay} - {$endDay} {$startMonth} {$toYear}";
}
// If different years, include both years
if ($differentYears) {
return "{$startDay} {$startMonth} {$fromYear} - {$endDay} {$endMonth} {$toYear}";
}
// Same year but different months
return "{$startDay} {$startMonth} - {$endDay} {$endMonth} {$toYear}";
default:
// Default format for unknown frequencies
return $from->format('j M Y') . ' - ' . $to->format('j M Y');
}
}
public function sanitizeFileName(string $fileName): string
{
return str_replace(['/', '\\'], '-', $fileName);
}
private function getPayload(AutomatedReportsService $automatedReportsService)
{
$reportResult = AutomatedReportResult::find(269);
$automatedReport = $reportResult->getReport();
$activityIds = [1,2,3];
$payload = $automatedReportsService->getAskJiminnyGenerateReportPayload(
automatedReport: $automatedReport,
reportResult: $reportResult,
activityIds: $activityIds,
);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$payload ' . PHP_EOL . print_r($payload, true));
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
36
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\DealInsights;
use Doctrine\DBAL\Connection;
use Generator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Jiminny\Component\DealInsights\Forecast\DealData;
use Jiminny\Component\DealInsights\Forecast\DealsFilter;
use Jiminny\Component\DealInsights\QueryBuilder\QueryBuilder;
use Jiminny\Component\DealInsights\QueryBuilder\Visitor\QueryBuilderVisitorInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Services\Crm\IntegrationApp\DTO\Utils\UrlGeneratorInterface;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class DealsRepository implements DealsRepositoryInterface
{
private Connection $connection;
private ProviderRegistry $providerRegistry;
/**
* @var QueryBuilderVisitorInterface[]
*/
private array $visitors = [];
/**
* @param QueryBuilderVisitorInterface[] $visitors
*/
public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])
{
$this->connection = $connection;
$this->providerRegistry = $crmProviderRegistry;
foreach ($visitors as $visitor) {
$this->visitors[$visitor->getIdentifier()] = $visitor;
}
}
public function getDeals(CriteriaInterface $criteria): array
{
$context = $criteria->getContext();
$team = $context->getTeam();
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$this->visit($qb, $criteria);
return $this->execute($team, $crmService, $qb);
}
public function getDeal(Team $team, int $id): array
{
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$qb->andWhere('opp.id = :id')->setParameter('id', $id);
return $this->execute($team, $crmService, $qb);
}
public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])
{
$qb = new QueryBuilder($this->connection);
$qb
->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')
->from('crm_fields', 'f')
->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')
->where('f.crm_configuration_id = :crm')
->andWhere('f.object_type = :type')
->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')
->orderBy('fd.object_id', 'ASC')
->addOrderBy('fd.updated_at', 'ASC')
->setParameter('type', Field::OBJECT_OPPORTUNITY)
->setParameter('crm', $crmId)
;
if (! empty($crmFields)) {
$fields = array_map(fn ($value): string => '"' . $value . '"', $crmFields);
$qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');
}
return $qb->executeQuery()->fetchAllAssociative();
}
public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAssociative();
}
public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('COALESCE(opp.currency_code, "' . $defaultCurrency . '") AS currency')
->addSelect('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
->groupBy('currency')
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAllAssociative();
}
public function getDealActivities(CriteriaInterface $criteria): array
{
$qb = Activity::with(['participants', 'user'])
->where('opportunity_id', $criteria->getOpportunityId())
->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())
->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())
->orderBy($criteria->getSortBy(), $criteria->getSortDirection())
;
// Should we filter activities by criteria? It's intended to filter deals.
return $qb->get()->all();
}
public function getStages(CriteriaInterface $criteria): array
{
$qb = new QueryBuilder($this->connection);
$qb
->select('id', 'label', 'sequence')
->from('stages', 's')
->where('crm_configuration_id = :crm_configuration_id')
->andWhere('type = :type')
->orderBy('sequence', 'ASC')
->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())
->setParameter('type', Stage::TYPE_OPPORTUNITY);
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$result[$row['id']] = [
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
public function getConfigurationStages(Configuration $configuration): Collection
{
return $configuration
->stages()
->where('type', Stage::TYPE_OPPORTUNITY)
->get();
}
public function getPipelineData(Configuration $crm): array
{
$qb = new QueryBuilder($this->connection);
$provider = $crm->provider;
$qb
->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')
->from('stages', 's')
->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')
->where('s.crm_configuration_id = :crm_configuration_id')
->andWhere('s.type = :type')
->orderBy('bps.business_process_id', 'ASC')
->addOrderBy('s.sequence', 'ASC')
->setParameter('crm_configuration_id', $crm->id)
->setParameter('type', Stage::TYPE_OPPORTUNITY)
;
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];
$result[$row['pipeline_id']][] = [
'value' => $value,
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
private function createQueryBuilder(string $realm): QueryBuilder
{
return (new QueryBuilder($this->connection))
->setRealm($realm)
->from('opportunities', 'opp')
->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')
->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')
->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')
;
}
/**
* Applies all applicable visitors and returns the IDs of the executed ones
*
* @return string[]
*/
private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array
{
$queryVisitors = [];
foreach ($this->visitors as $visitor) {
if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {
$visitor->visit($queryBuilder, $criteria);
$queryVisitors[] = $visitor->getIdentifier();
}
}
return $queryVisitors;
}
private function hydrateStages(array $deals): array
{
foreach ($this->fetchStages(array_keys($deals)) as $stage) {
$oppId = (int) $stage['opportunity_id'];
if (! isset($deals[$oppId])) {
continue; // or throw??!
}
$deals[$oppId]['stages'][] = [
'id' => $stage['stage_id'],
'name' => $stage['label'],
'enteredAt' => $stage['created_at'],
];
}
return $deals;
}
/**
* @param int[] $dealIds
*/
private function fetchStages(array $dealIds): array
{
if (empty($dealIds)) {
return [];
}
$qb = new QueryBuilder($this->connection);
$qb
->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')
->from('opportunity_stages', 'os')
->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')
->where($qb->expr()->in('os.opportunity_id', $dealIds))
->orderBy('os.opportunity_id', 'ASC')
->addOrderBy('s.created_at', 'ASC')
;
return $qb->executeQuery()->fetchAllAssociative();
}
private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array
{
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$data = [
'uuid' => RequiresUUID::toNormal($row['uuid']),
'name' => $row['name'],
'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),
'account' => [
'name' => $row['acc_name'],
'url' => $crmService->generateProviderUrl(
providerId: $row['acc_provider_id'],
objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'
),
],
'owner' => null,
'rawValue' => [
'amount' => (float) $row['value'],
'currency' => $row['currency_code'],
],
'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),
'openDate' => $row['remotely_created_at'] ?? null,
'closeDate' => $row['close_date'] ?? null,
'stages' => [],
'currentPipelineId' => $row['pipeline_id'],
'currentStage' => [
'id' => $row['stage_id'],
'enteredAt' => $row['stage_updated_at'],
],
'currentStageUpdatedAt' => $row['stage_updated_at'],
'isClosed' => (bool) $row['is_closed'],
'isWon' => (bool) $row['is_won'],
];
if (isset($row['owner_uuid'])) {
$data['owner'] = [
'uuid' => RequiresUUID::toNormal($row['owner_uuid']),
'name' => $row['owner_name'],
'photoUrl' => $row['owner_photo'] === null
? null
: client_cdn($row['owner_photo'], $team),
'id' => $row['owner_id'],
'job' => $row['owner_job'],
];
}
$result[(int) $row['opp_id']] = $data;
}
return $this->hydrateStages($result);
}
private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder
{
$qb = clone $queryBuilder;
$qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');
$qb
->select(...[
'opp.id as opp_id',
'opp.uuid',
'opp.name',
'opp.value',
'opp.currency_code',
'opp.close_date',
'opp.remotely_created_at',
'opp.is_closed',
'opp.is_won',
])
->addSelect(...[
'usr.uuid as owner_uuid',
'usr.name AS owner_name',
'usr.photo_path as owner_photo',
'usr.id AS owner_id',
'jt.name as owner_job',
])
->addSelect('opp.stage_id', 'opp.stage_updated_at')
->addSelect(...[
'acc.name AS acc_name',
'acc.is_internal as acc_is_internal',
'opp.stage_updated_at',
'acc.crm_provider_id AS acc_provider_id',
'opp.crm_provider_id AS opp_provider_id',
])
->addSelect('rt.business_process_id AS pipeline_id')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'));
return $qb;
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws SocialAccountTokenInvalidException
*/
private function getCrmService(Team $team): ServiceInterface
{
$crmService = $this->providerRegistry->get($team->crm->provider);
$crmService->setConfiguration($team->crm);
if ($crmService instanceof UrlGeneratorInterface) {
$crmService->setCrmUrlGenerator($team->crm);
}
return $crmService;
}
/**
*
* @return Generator<DealData>
*/
public function getForecastData(DealsFilter $filter): Generator
{
$opportunities = DB::query()
->select([
'o.value',
'o.close_date',
'o.currency_code',
'o.is_won',
'o.is_closed',
'o.probability',
'o.forecast_category',
])
->from('opportunities', 'o')
->join('users', 'users.id', '=', 'o.user_id')
->join('groups', 'groups.id', '=', 'users.group_id')
->where('users.team_id', $filter->getTeam()->getId())
->where('o.close_date', '>=', $filter->getStartDate())
->where('o.close_date', '<=', $filter->getEndDate())
->where('o.currency_code', $filter->getCurrency())
->where('o.deleted_at', '=', null)
;
$userUuidList = $filter->getUserUuidList();
if (! empty($userUuidList)) {
$userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);
$opportunities->whereIn('users.uuid', $userUuidList);
}
$groupUuidList = $filter->getGroupUuidList();
if (! empty($groupUuidList)) {
$groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);
$opportunities->whereIn('groups.uuid', $groupUuidList);
}
foreach ($opportunities->cursor() as $row) {
yield new DealData(
(float) $row->value,
$row->close_date,
! empty($row->is_won),
! empty($row->is_closed),
$row->probability ?: 0,
$row->forecast_category ?: '',
);
}
}
public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection
{
return $user->subscriptionSets()
->where(static function (Eloquent\Builder $query): void {
$query
->whereNull('expired_at')
->orWhere('expired_at', '>=', now());
})
->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {
$join
->on('subscription_set_id', '=', 'activity_subscription_sets.id');
$join
->where('followable_type', Models\Activity\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)
->whereIn('followable_id', $opportunityIds);
})
->pluck('followable_id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
|
73513
|
NULL
|
0
|
2026-04-23T07:51:34.436344+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776930694436_m1.jpg...
|
PhpStorm
|
faVsco.js – JiminnyDebugCommand.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
111
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands;
use Carbon\Carbon;
use Illuminate\Console\Command;
use InvalidArgumentException;
use Jiminny\Jobs\AutomatedReports\RequestGenerateAskJiminnyReportJob;
use Jiminny\Jobs\AutomatedReports\SendReportMailJob;
use Jiminny\Jobs\JobDispatcherInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
/**
* Class JiminnyDebugCommand
*
* @package Jiminny\Console\Commands
*/
class JiminnyDebugCommand extends Command
{
public const string FREQUENCY_DAILY = 'daily';
public const string FREQUENCY_WEEKLY = 'weekly';
public const string FREQUENCY_MONTHLY = 'monthly';
public const string FREQUENCY_QUARTERLY = 'quarterly';
public const string FREQUENCY_ONE_OFF = 'one_off';
protected $signature = 'jiminny:debug';
public function handle(JobDispatcherInterface $jobDispatcher, AutomatedReportsService $automatedReportsService): void
{
exit(1);
$now = Carbon::now()->subDay(5);
$from = $now->copy()->previousWeekday()->startOfDay();
$to = $now->copy()->previousWeekday()->endOfDay();
$this->info("Now: {$now->toDateTimeString()}");
$this->info("From: {$from->toDateTimeString()}");
$this->info("To: {$to->toDateTimeString()}");
exit(1);
$report = AutomatedReport::find(71);
$job = new RequestGenerateAskJiminnyReportJob($report->getUuid());
$jobDispatcher->dispatch($job);
exit(1);
// $this->formatDate($jobDispatcher);
// $this->sendMail($jobDispatcher, $automatedReportsService);
// $this->crmService();
$this->getPayload($automatedReportsService);
exit(1);
}
private function crmService()
{
$activity = Activity::find(418141);
$team = Team::find(19);
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
$crmService->createTranscriptNotes($activity);
}
private function sendMail(JobDispatcherInterface $jobDispatcher, AutomatedReportsService $automatedReportsService)
{
$reportUuid = '';
// $report = $automatedReportsService->getReportResult($reportUuid);
$report = AutomatedReportResult::find(275);
$validRecipients = $automatedReportsService->getValidRecipientUsers(
$report->getReport(),
includeJiminny: true,
);
$recipient = $validRecipients[0];
$fileName = $automatedReportsService->getReportFileName($report);
$typeName = $report->getReport()->getCustomName()
?? $automatedReportsService->getReportTypeName($report);
$teamsName = $automatedReportsService->getReportTeamsName($report);
$periodName = $automatedReportsService->getReportPeriodName($report);
$s3Path = $automatedReportsService->getMediaPath($report);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$fileName ' . PHP_EOL . print_r($fileName, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$typeName ' . PHP_EOL . print_r($typeName, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$teamsName ' . PHP_EOL . print_r($teamsName, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$periodName ' . PHP_EOL . print_r($periodName, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$s3Path ' . PHP_EOL . print_r($s3Path, true));
$jobDispatcher->dispatch(
new SendReportMailJob(
reportUuid: $report->getUuid(),
s3Path: $s3Path,
recipientEmail: $recipient['email'],
recipientName: $recipient['name'] ?? null,
fileName: $fileName,
typeName: $typeName,
teamsName: $teamsName,
periodName: $periodName,
isAskJiminny: true,
)
);
exit(1);
}
private function formatDate(JobDispatcherInterface $jobDispatcher): void
{
$customName = 'Custom report name';
// $frequency = self::FREQUENCY_DAILY;
// $frequency = self::FREQUENCY_WEEKLY;
$frequency = self::FREQUENCY_MONTHLY;
// $frequency = self::FREQUENCY_QUARTERLY;
// $frequency = self::FREQUENCY_ONE_OFF;
$period = $this->calculateFromAndToDatePeriod($frequency);
$from = $period['fromDate'];
$to = $period['toDate'];
$periodName = $this->formatReportPeriodName($frequency, $from, $to);
$filenameSuffix = null;
if ($customName) {
if ($filenameSuffix) {
$customName .= " {$filenameSuffix}";
}
$result = $this->sanitizeFileName("{$customName} - {$periodName}");
}
$this->info($result);
}
public function calculateFromAndToDatePeriod(
string $frequency,
?Carbon $fromDate = null,
?Carbon $toDate = null
): array {
if ($frequency === self::FREQUENCY_ONE_OFF) {
return [
'fromDate' => $fromDate,
'toDate' => $toDate,
];
}
$now = Carbon::now();
return match ($frequency) {
self::FREQUENCY_DAILY => [
'fromDate' => $now->copy()->subDay()->startOfDay(),
'toDate' => $now->copy()->subDay()->endOfDay(),
],
self::FREQUENCY_WEEKLY => [
'fromDate' => $now->copy()->subWeeks(1)->startOfDay(),
'toDate' => $now->copy()->subDay()->endOfDay(),
],
self::FREQUENCY_MONTHLY => [
'fromDate' => $now->copy()->subMonths(1)->startOfDay(),
'toDate' => $now->copy()->subDay()->endOfDay(),
],
self::FREQUENCY_QUARTERLY => [
'fromDate' => $now->copy()->subMonths(3)->startOfDay(),
'toDate' => $now->copy()->subDay()->endOfDay(),
],
default => throw new InvalidArgumentException("Unsupported frequency: {$frequency}"),
};
}
private function formatReportPeriodName(string $frequency, Carbon $from, Carbon $to): string
{
$fromYear = $from->format('Y');
$toYear = $to->format('Y');
$differentYears = $fromYear !== $toYear;
switch ($frequency) {
case self::FREQUENCY_DAILY:
return $from->format('j M Y');
case self::FREQUENCY_QUARTERLY:
// 'Jan-Mar 2025' or 'Nov 2024-Jan 2025' if years differ
$startMonth = $from->format('M');
$endMonth = $to->copy()->subMonth();
$endMonthName = $endMonth->format('M');
$endMonthYear = $endMonth->format('Y');
if ($differentYears) {
return "{$startMonth} {$fromYear} - {$endMonthName} {$endMonthYear}";
}
return "{$startMonth} - {$endMonthName} {$toYear}";
case self::FREQUENCY_MONTHLY:
// 'May 2025' - monthly reports are always within the same year
return $from->format('M Y');
case self::FREQUENCY_WEEKLY:
// '4 - 8 Aug 2025', '27 Oct - 3 Nov 2025', or '28 Dec 2024 - 3 Jan 2025' if years differ
$startDay = $from->format('j');
$endDay = $to->format('j');
$startMonth = $from->format('M');
$endMonth = $to->format('M');
if ($differentYears) {
return "{$startDay} {$startMonth} {$fromYear} - {$endDay} {$endMonth} {$toYear}";
}
if ($startMonth !== $endMonth) {
return "{$startDay} {$startMonth} - {$endDay} {$endMonth} {$toYear}";
}
return "{$startDay} - {$endDay} {$endMonth} {$toYear}";
case self::FREQUENCY_ONE_OFF:
// '2 May-31 May 2025' or '15 Dec 2024-15 Jan 2025' if years differ
$startDay = $from->format('j');
$startMonth = $from->format('M');
$endDay = $to->format('j');
$endMonth = $to->format('M');
// If same month and year, use a format like '2-31 May 2025'
if ($startMonth === $endMonth && ! $differentYears) {
return "{$startDay} - {$endDay} {$startMonth} {$toYear}";
}
// If different years, include both years
if ($differentYears) {
return "{$startDay} {$startMonth} {$fromYear} - {$endDay} {$endMonth} {$toYear}";
}
// Same year but different months
return "{$startDay} {$startMonth} - {$endDay} {$endMonth} {$toYear}";
default:
// Default format for unknown frequencies
return $from->format('j M Y') . ' - ' . $to->format('j M Y');
}
}
public function sanitizeFileName(string $fileName): string
{
return str_replace(['/', '\\'], '-', $fileName);
}
private function getPayload(AutomatedReportsService $automatedReportsService)
{
$reportResult = AutomatedReportResult::find(269);
$automatedReport = $reportResult->getReport();
$activityIds = [1,2,3];
$payload = $automatedReportsService->getAskJiminnyGenerateReportPayload(
automatedReport: $automatedReport,
reportResult: $reportResult,
activityIds: $activityIds,
);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$payload ' . PHP_EOL . print_r($payload, true));
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
36
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\DealInsights;
use Doctrine\DBAL\Connection;
use Generator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Jiminny\Component\DealInsights\Forecast\DealData;
use Jiminny\Component\DealInsights\Forecast\DealsFilter;
use Jiminny\Component\DealInsights\QueryBuilder\QueryBuilder;
use Jiminny\Component\DealInsights\QueryBuilder\Visitor\QueryBuilderVisitorInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Services\Crm\IntegrationApp\DTO\Utils\UrlGeneratorInterface;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class DealsRepository implements DealsRepositoryInterface
{
private Connection $connection;
private ProviderRegistry $providerRegistry;
/**
* @var QueryBuilderVisitorInterface[]
*/
private array $visitors = [];
/**
* @param QueryBuilderVisitorInterface[] $visitors
*/
public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])
{
$this->connection = $connection;
$this->providerRegistry = $crmProviderRegistry;
foreach ($visitors as $visitor) {
$this->visitors[$visitor->getIdentifier()] = $visitor;
}
}
public function getDeals(CriteriaInterface $criteria): array
{
$context = $criteria->getContext();
$team = $context->getTeam();
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$this->visit($qb, $criteria);
return $this->execute($team, $crmService, $qb);
}
public function getDeal(Team $team, int $id): array
{
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$qb->andWhere('opp.id = :id')->setParameter('id', $id);
return $this->execute($team, $crmService, $qb);
}
public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])
{
$qb = new QueryBuilder($this->connection);
$qb
->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')
->from('crm_fields', 'f')
->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')
->where('f.crm_configuration_id = :crm')
->andWhere('f.object_type = :type')
->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')
->orderBy('fd.object_id', 'ASC')
->addOrderBy('fd.updated_at', 'ASC')
->setParameter('type', Field::OBJECT_OPPORTUNITY)
->setParameter('crm', $crmId)
;
if (! empty($crmFields)) {
$fields = array_map(fn ($value): string => '"' . $value . '"', $crmFields);
$qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');
}
return $qb->executeQuery()->fetchAllAssociative();
}
public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAssociative();
}
public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('COALESCE(opp.currency_code, "' . $defaultCurrency . '") AS currency')
->addSelect('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
->groupBy('currency')
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAllAssociative();
}
public function getDealActivities(CriteriaInterface $criteria): array
{
$qb = Activity::with(['participants', 'user'])
->where('opportunity_id', $criteria->getOpportunityId())
->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())
->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())
->orderBy($criteria->getSortBy(), $criteria->getSortDirection())
;
// Should we filter activities by criteria? It's intended to filter deals.
return $qb->get()->all();
}
public function getStages(CriteriaInterface $criteria): array
{
$qb = new QueryBuilder($this->connection);
$qb
->select('id', 'label', 'sequence')
->from('stages', 's')
->where('crm_configuration_id = :crm_configuration_id')
->andWhere('type = :type')
->orderBy('sequence', 'ASC')
->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())
->setParameter('type', Stage::TYPE_OPPORTUNITY);
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$result[$row['id']] = [
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
public function getConfigurationStages(Configuration $configuration): Collection
{
return $configuration
->stages()
->where('type', Stage::TYPE_OPPORTUNITY)
->get();
}
public function getPipelineData(Configuration $crm): array
{
$qb = new QueryBuilder($this->connection);
$provider = $crm->provider;
$qb
->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')
->from('stages', 's')
->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')
->where('s.crm_configuration_id = :crm_configuration_id')
->andWhere('s.type = :type')
->orderBy('bps.business_process_id', 'ASC')
->addOrderBy('s.sequence', 'ASC')
->setParameter('crm_configuration_id', $crm->id)
->setParameter('type', Stage::TYPE_OPPORTUNITY)
;
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];
$result[$row['pipeline_id']][] = [
'value' => $value,
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
private function createQueryBuilder(string $realm): QueryBuilder
{
return (new QueryBuilder($this->connection))
->setRealm($realm)
->from('opportunities', 'opp')
->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')
->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')
->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')
;
}
/**
* Applies all applicable visitors and returns the IDs of the executed ones
*
* @return string[]
*/
private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array
{
$queryVisitors = [];
foreach ($this->visitors as $visitor) {
if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {
$visitor->visit($queryBuilder, $criteria);
$queryVisitors[] = $visitor->getIdentifier();
}
}
return $queryVisitors;
}
private function hydrateStages(array $deals): array
{
foreach ($this->fetchStages(array_keys($deals)) as $stage) {
$oppId = (int) $stage['opportunity_id'];
if (! isset($deals[$oppId])) {
continue; // or throw??!
}
$deals[$oppId]['stages'][] = [
'id' => $stage['stage_id'],
'name' => $stage['label'],
'enteredAt' => $stage['created_at'],
];
}
return $deals;
}
/**
* @param int[] $dealIds
*/
private function fetchStages(array $dealIds): array
{
if (empty($dealIds)) {
return [];
}
$qb = new QueryBuilder($this->connection);
$qb
->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')
->from('opportunity_stages', 'os')
->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')
->where($qb->expr()->in('os.opportunity_id', $dealIds))
->orderBy('os.opportunity_id', 'ASC')
->addOrderBy('s.created_at', 'ASC')
;
return $qb->executeQuery()->fetchAllAssociative();
}
private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array
{
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$data = [
'uuid' => RequiresUUID::toNormal($row['uuid']),
'name' => $row['name'],
'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),
'account' => [
'name' => $row['acc_name'],
'url' => $crmService->generateProviderUrl(
providerId: $row['acc_provider_id'],
objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'
),
],
'owner' => null,
'rawValue' => [
'amount' => (float) $row['value'],
'currency' => $row['currency_code'],
],
'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),
'openDate' => $row['remotely_created_at'] ?? null,
'closeDate' => $row['close_date'] ?? null,
'stages' => [],
'currentPipelineId' => $row['pipeline_id'],
'currentStage' => [
'id' => $row['stage_id'],
'enteredAt' => $row['stage_updated_at'],
],
'currentStageUpdatedAt' => $row['stage_updated_at'],
'isClosed' => (bool) $row['is_closed'],
'isWon' => (bool) $row['is_won'],
];
if (isset($row['owner_uuid'])) {
$data['owner'] = [
'uuid' => RequiresUUID::toNormal($row['owner_uuid']),
'name' => $row['owner_name'],
'photoUrl' => $row['owner_photo'] === null
? null
: client_cdn($row['owner_photo'], $team),
'id' => $row['owner_id'],
'job' => $row['owner_job'],
];
}
$result[(int) $row['opp_id']] = $data;
}
return $this->hydrateStages($result);
}
private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder
{
$qb = clone $queryBuilder;
$qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');
$qb
->select(...[
'opp.id as opp_id',
'opp.uuid',
'opp.name',
'opp.value',
'opp.currency_code',
'opp.close_date',
'opp.remotely_created_at',
'opp.is_closed',
'opp.is_won',
])
->addSelect(...[
'usr.uuid as owner_uuid',
'usr.name AS owner_name',
'usr.photo_path as owner_photo',
'usr.id AS owner_id',
'jt.name as owner_job',
])
->addSelect('opp.stage_id', 'opp.stage_updated_at')
->addSelect(...[
'acc.name AS acc_name',
'acc.is_internal as acc_is_internal',
'opp.stage_updated_at',
'acc.crm_provider_id AS acc_provider_id',
'opp.crm_provider_id AS opp_provider_id',
])
->addSelect('rt.business_process_id AS pipeline_id')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'));
return $qb;
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws SocialAccountTokenInvalidException
*/
private function getCrmService(Team $team): ServiceInterface
{
$crmService = $this->providerRegistry->get($team->crm->provider);
$crmService->setConfiguration($team->crm);
if ($crmService instanceof UrlGeneratorInterface) {
$crmService->setCrmUrlGenerator($team->crm);
}
return $crmService;
}
/**
*
* @return Generator<DealData>
*/
public function getForecastData(DealsFilter $filter): Generator
{
$opportunities = DB::query()
->select([
'o.value',
'o.close_date',
'o.currency_code',
'o.is_won',
'o.is_closed',
'o.probability',
'o.forecast_category',
])
->from('opportunities', 'o')
->join('users', 'users.id', '=', 'o.user_id')
->join('groups', 'groups.id', '=', 'users.group_id')
->where('users.team_id', $filter->getTeam()->getId())
->where('o.close_date', '>=', $filter->getStartDate())
->where('o.close_date', '<=', $filter->getEndDate())
->where('o.currency_code', $filter->getCurrency())
->where('o.deleted_at', '=', null)
;
$userUuidList = $filter->getUserUuidList();
if (! empty($userUuidList)) {
$userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);
$opportunities->whereIn('users.uuid', $userUuidList);
}
$groupUuidList = $filter->getGroupUuidList();
if (! empty($groupUuidList)) {
$groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);
$opportunities->whereIn('groups.uuid', $groupUuidList);
}
foreach ($opportunities->cursor() as $row) {
yield new DealData(
(float) $row->value,
$row->close_date,
! empty($row->is_won),
! empty($row->is_closed),
$row->probability ?: 0,
$row->forecast_category ?: '',
);
}
}
public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection
{
return $user->subscriptionSets()
->where(static function (Eloquent\Builder $query): void {
$query
->whereNull('expired_at')
->orWhere('expired_at', '>=', now());
})
->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {
$join
->on('subscription_set_id', '=', 'activity_subscription_sets.id');
$join
->where('followable_type', Models\Activity\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)
->whereIn('followable_id', $opportunityIds);
})
->pluck('followable_id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20157-AJ-report-not-send-notification, menu","depth":5,"help_text":"Git Branch: JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"RequestGenerateAskJiminnyReportJobTest","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"111","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Console\\Command;\nuse InvalidArgumentException;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateAskJiminnyReportJob;\nuse Jiminny\\Jobs\\AutomatedReports\\SendReportMailJob;\nuse Jiminny\\Jobs\\JobDispatcherInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\n\n/**\n * Class JiminnyDebugCommand\n *\n * @package Jiminny\\Console\\Commands\n */\nclass JiminnyDebugCommand extends Command\n{\n public const string FREQUENCY_DAILY = 'daily';\n public const string FREQUENCY_WEEKLY = 'weekly';\n public const string FREQUENCY_MONTHLY = 'monthly';\n public const string FREQUENCY_QUARTERLY = 'quarterly';\n public const string FREQUENCY_ONE_OFF = 'one_off';\n protected $signature = 'jiminny:debug';\n\n public function handle(JobDispatcherInterface $jobDispatcher, AutomatedReportsService $automatedReportsService): void\n {\n \n \n \n exit(1);\n \n $now = Carbon::now()->subDay(5);\n $from = $now->copy()->previousWeekday()->startOfDay();\n $to = $now->copy()->previousWeekday()->endOfDay();\n\n $this->info(\"Now: {$now->toDateTimeString()}\");\n $this->info(\"From: {$from->toDateTimeString()}\");\n $this->info(\"To: {$to->toDateTimeString()}\");\n\n exit(1);\n\n $report = AutomatedReport::find(71);\n\n $job = new RequestGenerateAskJiminnyReportJob($report->getUuid());\n $jobDispatcher->dispatch($job);\n\n exit(1);\n\n\n // $this->formatDate($jobDispatcher);\n // $this->sendMail($jobDispatcher, $automatedReportsService);\n // $this->crmService();\n\n $this->getPayload($automatedReportsService);\n\n exit(1);\n }\n\n\n\n private function crmService()\n {\n $activity = Activity::find(418141);\n\n $team = Team::find(19);\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n $crmService = $crmResolver->prepareCrmService();\n\n $crmService->createTranscriptNotes($activity);\n }\n\n private function sendMail(JobDispatcherInterface $jobDispatcher, AutomatedReportsService $automatedReportsService)\n {\n $reportUuid = '';\n // $report = $automatedReportsService->getReportResult($reportUuid);\n $report = AutomatedReportResult::find(275);\n $validRecipients = $automatedReportsService->getValidRecipientUsers(\n $report->getReport(),\n includeJiminny: true,\n );\n\n $recipient = $validRecipients[0];\n\n $fileName = $automatedReportsService->getReportFileName($report);\n $typeName = $report->getReport()->getCustomName()\n ?? $automatedReportsService->getReportTypeName($report);\n $teamsName = $automatedReportsService->getReportTeamsName($report);\n $periodName = $automatedReportsService->getReportPeriodName($report);\n $s3Path = $automatedReportsService->getMediaPath($report);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$fileName ' . PHP_EOL . print_r($fileName, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$typeName ' . PHP_EOL . print_r($typeName, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$teamsName ' . PHP_EOL . print_r($teamsName, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$periodName ' . PHP_EOL . print_r($periodName, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$s3Path ' . PHP_EOL . print_r($s3Path, true));\n\n $jobDispatcher->dispatch(\n new SendReportMailJob(\n reportUuid: $report->getUuid(),\n s3Path: $s3Path,\n recipientEmail: $recipient['email'],\n recipientName: $recipient['name'] ?? null,\n fileName: $fileName,\n typeName: $typeName,\n teamsName: $teamsName,\n periodName: $periodName,\n isAskJiminny: true,\n )\n );\n\n exit(1);\n }\n\n private function formatDate(JobDispatcherInterface $jobDispatcher): void\n {\n $customName = 'Custom report name';\n // $frequency = self::FREQUENCY_DAILY;\n // $frequency = self::FREQUENCY_WEEKLY;\n $frequency = self::FREQUENCY_MONTHLY;\n // $frequency = self::FREQUENCY_QUARTERLY;\n // $frequency = self::FREQUENCY_ONE_OFF;\n $period = $this->calculateFromAndToDatePeriod($frequency);\n $from = $period['fromDate'];\n $to = $period['toDate'];\n $periodName = $this->formatReportPeriodName($frequency, $from, $to);\n $filenameSuffix = null;\n\n if ($customName) {\n if ($filenameSuffix) {\n $customName .= \" {$filenameSuffix}\";\n }\n\n $result = $this->sanitizeFileName(\"{$customName} - {$periodName}\");\n }\n\n $this->info($result);\n }\n\n public function calculateFromAndToDatePeriod(\n string $frequency,\n ?Carbon $fromDate = null,\n ?Carbon $toDate = null\n ): array {\n if ($frequency === self::FREQUENCY_ONE_OFF) {\n return [\n 'fromDate' => $fromDate,\n 'toDate' => $toDate,\n ];\n }\n\n $now = Carbon::now();\n\n return match ($frequency) {\n self::FREQUENCY_DAILY => [\n 'fromDate' => $now->copy()->subDay()->startOfDay(),\n 'toDate' => $now->copy()->subDay()->endOfDay(),\n ],\n self::FREQUENCY_WEEKLY => [\n 'fromDate' => $now->copy()->subWeeks(1)->startOfDay(),\n 'toDate' => $now->copy()->subDay()->endOfDay(),\n ],\n self::FREQUENCY_MONTHLY => [\n 'fromDate' => $now->copy()->subMonths(1)->startOfDay(),\n 'toDate' => $now->copy()->subDay()->endOfDay(),\n ],\n self::FREQUENCY_QUARTERLY => [\n 'fromDate' => $now->copy()->subMonths(3)->startOfDay(),\n 'toDate' => $now->copy()->subDay()->endOfDay(),\n ],\n default => throw new InvalidArgumentException(\"Unsupported frequency: {$frequency}\"),\n };\n }\n\n private function formatReportPeriodName(string $frequency, Carbon $from, Carbon $to): string\n {\n $fromYear = $from->format('Y');\n $toYear = $to->format('Y');\n $differentYears = $fromYear !== $toYear;\n\n switch ($frequency) {\n case self::FREQUENCY_DAILY:\n return $from->format('j M Y');\n\n case self::FREQUENCY_QUARTERLY:\n // 'Jan-Mar 2025' or 'Nov 2024-Jan 2025' if years differ\n $startMonth = $from->format('M');\n $endMonth = $to->copy()->subMonth();\n $endMonthName = $endMonth->format('M');\n $endMonthYear = $endMonth->format('Y');\n\n if ($differentYears) {\n return \"{$startMonth} {$fromYear} - {$endMonthName} {$endMonthYear}\";\n }\n\n return \"{$startMonth} - {$endMonthName} {$toYear}\";\n\n case self::FREQUENCY_MONTHLY:\n // 'May 2025' - monthly reports are always within the same year\n return $from->format('M Y');\n\n case self::FREQUENCY_WEEKLY:\n // '4 - 8 Aug 2025', '27 Oct - 3 Nov 2025', or '28 Dec 2024 - 3 Jan 2025' if years differ\n $startDay = $from->format('j');\n $endDay = $to->format('j');\n $startMonth = $from->format('M');\n $endMonth = $to->format('M');\n\n if ($differentYears) {\n return \"{$startDay} {$startMonth} {$fromYear} - {$endDay} {$endMonth} {$toYear}\";\n }\n\n if ($startMonth !== $endMonth) {\n return \"{$startDay} {$startMonth} - {$endDay} {$endMonth} {$toYear}\";\n }\n\n return \"{$startDay} - {$endDay} {$endMonth} {$toYear}\";\n\n case self::FREQUENCY_ONE_OFF:\n // '2 May-31 May 2025' or '15 Dec 2024-15 Jan 2025' if years differ\n $startDay = $from->format('j');\n $startMonth = $from->format('M');\n $endDay = $to->format('j');\n $endMonth = $to->format('M');\n\n // If same month and year, use a format like '2-31 May 2025'\n if ($startMonth === $endMonth && ! $differentYears) {\n return \"{$startDay} - {$endDay} {$startMonth} {$toYear}\";\n }\n\n // If different years, include both years\n if ($differentYears) {\n return \"{$startDay} {$startMonth} {$fromYear} - {$endDay} {$endMonth} {$toYear}\";\n }\n\n // Same year but different months\n return \"{$startDay} {$startMonth} - {$endDay} {$endMonth} {$toYear}\";\n\n default:\n // Default format for unknown frequencies\n return $from->format('j M Y') . ' - ' . $to->format('j M Y');\n }\n }\n\n public function sanitizeFileName(string $fileName): string\n {\n return str_replace(['/', '\\\\'], '-', $fileName);\n }\n\n private function getPayload(AutomatedReportsService $automatedReportsService)\n {\n $reportResult = AutomatedReportResult::find(269);\n $automatedReport = $reportResult->getReport();\n $activityIds = [1,2,3];\n $payload = $automatedReportsService->getAskJiminnyGenerateReportPayload(\n automatedReport: $automatedReport,\n reportResult: $reportResult,\n activityIds: $activityIds,\n );\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$payload ' . PHP_EOL . print_r($payload, true));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Console\\Command;\nuse InvalidArgumentException;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateAskJiminnyReportJob;\nuse Jiminny\\Jobs\\AutomatedReports\\SendReportMailJob;\nuse Jiminny\\Jobs\\JobDispatcherInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\n\n/**\n * Class JiminnyDebugCommand\n *\n * @package Jiminny\\Console\\Commands\n */\nclass JiminnyDebugCommand extends Command\n{\n public const string FREQUENCY_DAILY = 'daily';\n public const string FREQUENCY_WEEKLY = 'weekly';\n public const string FREQUENCY_MONTHLY = 'monthly';\n public const string FREQUENCY_QUARTERLY = 'quarterly';\n public const string FREQUENCY_ONE_OFF = 'one_off';\n protected $signature = 'jiminny:debug';\n\n public function handle(JobDispatcherInterface $jobDispatcher, AutomatedReportsService $automatedReportsService): void\n {\n \n \n \n exit(1);\n \n $now = Carbon::now()->subDay(5);\n $from = $now->copy()->previousWeekday()->startOfDay();\n $to = $now->copy()->previousWeekday()->endOfDay();\n\n $this->info(\"Now: {$now->toDateTimeString()}\");\n $this->info(\"From: {$from->toDateTimeString()}\");\n $this->info(\"To: {$to->toDateTimeString()}\");\n\n exit(1);\n\n $report = AutomatedReport::find(71);\n\n $job = new RequestGenerateAskJiminnyReportJob($report->getUuid());\n $jobDispatcher->dispatch($job);\n\n exit(1);\n\n\n // $this->formatDate($jobDispatcher);\n // $this->sendMail($jobDispatcher, $automatedReportsService);\n // $this->crmService();\n\n $this->getPayload($automatedReportsService);\n\n exit(1);\n }\n\n\n\n private function crmService()\n {\n $activity = Activity::find(418141);\n\n $team = Team::find(19);\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n $crmService = $crmResolver->prepareCrmService();\n\n $crmService->createTranscriptNotes($activity);\n }\n\n private function sendMail(JobDispatcherInterface $jobDispatcher, AutomatedReportsService $automatedReportsService)\n {\n $reportUuid = '';\n // $report = $automatedReportsService->getReportResult($reportUuid);\n $report = AutomatedReportResult::find(275);\n $validRecipients = $automatedReportsService->getValidRecipientUsers(\n $report->getReport(),\n includeJiminny: true,\n );\n\n $recipient = $validRecipients[0];\n\n $fileName = $automatedReportsService->getReportFileName($report);\n $typeName = $report->getReport()->getCustomName()\n ?? $automatedReportsService->getReportTypeName($report);\n $teamsName = $automatedReportsService->getReportTeamsName($report);\n $periodName = $automatedReportsService->getReportPeriodName($report);\n $s3Path = $automatedReportsService->getMediaPath($report);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$fileName ' . PHP_EOL . print_r($fileName, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$typeName ' . PHP_EOL . print_r($typeName, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$teamsName ' . PHP_EOL . print_r($teamsName, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$periodName ' . PHP_EOL . print_r($periodName, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$s3Path ' . PHP_EOL . print_r($s3Path, true));\n\n $jobDispatcher->dispatch(\n new SendReportMailJob(\n reportUuid: $report->getUuid(),\n s3Path: $s3Path,\n recipientEmail: $recipient['email'],\n recipientName: $recipient['name'] ?? null,\n fileName: $fileName,\n typeName: $typeName,\n teamsName: $teamsName,\n periodName: $periodName,\n isAskJiminny: true,\n )\n );\n\n exit(1);\n }\n\n private function formatDate(JobDispatcherInterface $jobDispatcher): void\n {\n $customName = 'Custom report name';\n // $frequency = self::FREQUENCY_DAILY;\n // $frequency = self::FREQUENCY_WEEKLY;\n $frequency = self::FREQUENCY_MONTHLY;\n // $frequency = self::FREQUENCY_QUARTERLY;\n // $frequency = self::FREQUENCY_ONE_OFF;\n $period = $this->calculateFromAndToDatePeriod($frequency);\n $from = $period['fromDate'];\n $to = $period['toDate'];\n $periodName = $this->formatReportPeriodName($frequency, $from, $to);\n $filenameSuffix = null;\n\n if ($customName) {\n if ($filenameSuffix) {\n $customName .= \" {$filenameSuffix}\";\n }\n\n $result = $this->sanitizeFileName(\"{$customName} - {$periodName}\");\n }\n\n $this->info($result);\n }\n\n public function calculateFromAndToDatePeriod(\n string $frequency,\n ?Carbon $fromDate = null,\n ?Carbon $toDate = null\n ): array {\n if ($frequency === self::FREQUENCY_ONE_OFF) {\n return [\n 'fromDate' => $fromDate,\n 'toDate' => $toDate,\n ];\n }\n\n $now = Carbon::now();\n\n return match ($frequency) {\n self::FREQUENCY_DAILY => [\n 'fromDate' => $now->copy()->subDay()->startOfDay(),\n 'toDate' => $now->copy()->subDay()->endOfDay(),\n ],\n self::FREQUENCY_WEEKLY => [\n 'fromDate' => $now->copy()->subWeeks(1)->startOfDay(),\n 'toDate' => $now->copy()->subDay()->endOfDay(),\n ],\n self::FREQUENCY_MONTHLY => [\n 'fromDate' => $now->copy()->subMonths(1)->startOfDay(),\n 'toDate' => $now->copy()->subDay()->endOfDay(),\n ],\n self::FREQUENCY_QUARTERLY => [\n 'fromDate' => $now->copy()->subMonths(3)->startOfDay(),\n 'toDate' => $now->copy()->subDay()->endOfDay(),\n ],\n default => throw new InvalidArgumentException(\"Unsupported frequency: {$frequency}\"),\n };\n }\n\n private function formatReportPeriodName(string $frequency, Carbon $from, Carbon $to): string\n {\n $fromYear = $from->format('Y');\n $toYear = $to->format('Y');\n $differentYears = $fromYear !== $toYear;\n\n switch ($frequency) {\n case self::FREQUENCY_DAILY:\n return $from->format('j M Y');\n\n case self::FREQUENCY_QUARTERLY:\n // 'Jan-Mar 2025' or 'Nov 2024-Jan 2025' if years differ\n $startMonth = $from->format('M');\n $endMonth = $to->copy()->subMonth();\n $endMonthName = $endMonth->format('M');\n $endMonthYear = $endMonth->format('Y');\n\n if ($differentYears) {\n return \"{$startMonth} {$fromYear} - {$endMonthName} {$endMonthYear}\";\n }\n\n return \"{$startMonth} - {$endMonthName} {$toYear}\";\n\n case self::FREQUENCY_MONTHLY:\n // 'May 2025' - monthly reports are always within the same year\n return $from->format('M Y');\n\n case self::FREQUENCY_WEEKLY:\n // '4 - 8 Aug 2025', '27 Oct - 3 Nov 2025', or '28 Dec 2024 - 3 Jan 2025' if years differ\n $startDay = $from->format('j');\n $endDay = $to->format('j');\n $startMonth = $from->format('M');\n $endMonth = $to->format('M');\n\n if ($differentYears) {\n return \"{$startDay} {$startMonth} {$fromYear} - {$endDay} {$endMonth} {$toYear}\";\n }\n\n if ($startMonth !== $endMonth) {\n return \"{$startDay} {$startMonth} - {$endDay} {$endMonth} {$toYear}\";\n }\n\n return \"{$startDay} - {$endDay} {$endMonth} {$toYear}\";\n\n case self::FREQUENCY_ONE_OFF:\n // '2 May-31 May 2025' or '15 Dec 2024-15 Jan 2025' if years differ\n $startDay = $from->format('j');\n $startMonth = $from->format('M');\n $endDay = $to->format('j');\n $endMonth = $to->format('M');\n\n // If same month and year, use a format like '2-31 May 2025'\n if ($startMonth === $endMonth && ! $differentYears) {\n return \"{$startDay} - {$endDay} {$startMonth} {$toYear}\";\n }\n\n // If different years, include both years\n if ($differentYears) {\n return \"{$startDay} {$startMonth} {$fromYear} - {$endDay} {$endMonth} {$toYear}\";\n }\n\n // Same year but different months\n return \"{$startDay} {$startMonth} - {$endDay} {$endMonth} {$toYear}\";\n\n default:\n // Default format for unknown frequencies\n return $from->format('j M Y') . ' - ' . $to->format('j M Y');\n }\n }\n\n public function sanitizeFileName(string $fileName): string\n {\n return str_replace(['/', '\\\\'], '-', $fileName);\n }\n\n private function getPayload(AutomatedReportsService $automatedReportsService)\n {\n $reportResult = AutomatedReportResult::find(269);\n $automatedReport = $reportResult->getReport();\n $activityIds = [1,2,3];\n $payload = $automatedReportsService->getAskJiminnyGenerateReportPayload(\n automatedReport: $automatedReport,\n reportResult: $reportResult,\n activityIds: $activityIds,\n );\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$payload ' . PHP_EOL . print_r($payload, true));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"36","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\DealInsights;\n\nuse Doctrine\\DBAL\\Connection;\nuse Generator;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealData;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealsFilter;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\QueryBuilder;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\Visitor\\QueryBuilderVisitorInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Services\\Crm\\IntegrationApp\\DTO\\Utils\\UrlGeneratorInterface;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Eloquent;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass DealsRepository implements DealsRepositoryInterface\n{\n private Connection $connection;\n\n private ProviderRegistry $providerRegistry;\n\n /**\n * @var QueryBuilderVisitorInterface[]\n */\n private array $visitors = [];\n\n /**\n * @param QueryBuilderVisitorInterface[] $visitors\n */\n public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])\n {\n $this->connection = $connection;\n $this->providerRegistry = $crmProviderRegistry;\n\n foreach ($visitors as $visitor) {\n $this->visitors[$visitor->getIdentifier()] = $visitor;\n }\n }\n\n public function getDeals(CriteriaInterface $criteria): array\n {\n $context = $criteria->getContext();\n $team = $context->getTeam();\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n\n $this->visit($qb, $criteria);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getDeal(Team $team, int $id): array\n {\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n $qb->andWhere('opp.id = :id')->setParameter('id', $id);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')\n ->from('crm_fields', 'f')\n ->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')\n ->where('f.crm_configuration_id = :crm')\n ->andWhere('f.object_type = :type')\n ->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')\n ->orderBy('fd.object_id', 'ASC')\n ->addOrderBy('fd.updated_at', 'ASC')\n\n ->setParameter('type', Field::OBJECT_OPPORTUNITY)\n ->setParameter('crm', $crmId)\n ;\n\n if (! empty($crmFields)) {\n $fields = array_map(fn ($value): string => '\"' . $value . '\"', $crmFields);\n $qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');\n }\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAssociative();\n }\n\n public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('COALESCE(opp.currency_code, \"' . $defaultCurrency . '\") AS currency')\n ->addSelect('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ->groupBy('currency')\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getDealActivities(CriteriaInterface $criteria): array\n {\n $qb = Activity::with(['participants', 'user'])\n ->where('opportunity_id', $criteria->getOpportunityId())\n ->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())\n ->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())\n ->orderBy($criteria->getSortBy(), $criteria->getSortDirection())\n ;\n\n // Should we filter activities by criteria? It's intended to filter deals.\n\n return $qb->get()->all();\n }\n\n public function getStages(CriteriaInterface $criteria): array\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('id', 'label', 'sequence')\n ->from('stages', 's')\n ->where('crm_configuration_id = :crm_configuration_id')\n ->andWhere('type = :type')\n ->orderBy('sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())\n ->setParameter('type', Stage::TYPE_OPPORTUNITY);\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $result[$row['id']] = [\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n public function getConfigurationStages(Configuration $configuration): Collection\n {\n return $configuration\n ->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get();\n }\n\n public function getPipelineData(Configuration $crm): array\n {\n $qb = new QueryBuilder($this->connection);\n $provider = $crm->provider;\n\n $qb\n ->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')\n ->from('stages', 's')\n ->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')\n ->where('s.crm_configuration_id = :crm_configuration_id')\n ->andWhere('s.type = :type')\n ->orderBy('bps.business_process_id', 'ASC')\n ->addOrderBy('s.sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $crm->id)\n ->setParameter('type', Stage::TYPE_OPPORTUNITY)\n ;\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];\n $result[$row['pipeline_id']][] = [\n 'value' => $value,\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n private function createQueryBuilder(string $realm): QueryBuilder\n {\n return (new QueryBuilder($this->connection))\n ->setRealm($realm)\n ->from('opportunities', 'opp')\n ->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')\n ->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')\n ->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')\n ;\n }\n\n /**\n * Applies all applicable visitors and returns the IDs of the executed ones\n *\n * @return string[]\n */\n private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array\n {\n $queryVisitors = [];\n\n foreach ($this->visitors as $visitor) {\n if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {\n $visitor->visit($queryBuilder, $criteria);\n\n $queryVisitors[] = $visitor->getIdentifier();\n }\n }\n\n return $queryVisitors;\n }\n\n private function hydrateStages(array $deals): array\n {\n foreach ($this->fetchStages(array_keys($deals)) as $stage) {\n $oppId = (int) $stage['opportunity_id'];\n\n if (! isset($deals[$oppId])) {\n continue; // or throw??!\n }\n\n $deals[$oppId]['stages'][] = [\n 'id' => $stage['stage_id'],\n 'name' => $stage['label'],\n 'enteredAt' => $stage['created_at'],\n ];\n }\n\n return $deals;\n }\n\n /**\n * @param int[] $dealIds\n */\n private function fetchStages(array $dealIds): array\n {\n if (empty($dealIds)) {\n return [];\n }\n\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')\n ->from('opportunity_stages', 'os')\n ->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')\n ->where($qb->expr()->in('os.opportunity_id', $dealIds))\n ->orderBy('os.opportunity_id', 'ASC')\n ->addOrderBy('s.created_at', 'ASC')\n ;\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array\n {\n $result = [];\n\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $data = [\n 'uuid' => RequiresUUID::toNormal($row['uuid']),\n 'name' => $row['name'],\n 'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),\n 'account' => [\n 'name' => $row['acc_name'],\n 'url' => $crmService->generateProviderUrl(\n providerId: $row['acc_provider_id'],\n objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'\n ),\n ],\n 'owner' => null,\n 'rawValue' => [\n 'amount' => (float) $row['value'],\n 'currency' => $row['currency_code'],\n ],\n 'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),\n 'openDate' => $row['remotely_created_at'] ?? null,\n 'closeDate' => $row['close_date'] ?? null,\n 'stages' => [],\n 'currentPipelineId' => $row['pipeline_id'],\n 'currentStage' => [\n 'id' => $row['stage_id'],\n 'enteredAt' => $row['stage_updated_at'],\n ],\n 'currentStageUpdatedAt' => $row['stage_updated_at'],\n 'isClosed' => (bool) $row['is_closed'],\n 'isWon' => (bool) $row['is_won'],\n ];\n\n if (isset($row['owner_uuid'])) {\n $data['owner'] = [\n 'uuid' => RequiresUUID::toNormal($row['owner_uuid']),\n 'name' => $row['owner_name'],\n 'photoUrl' => $row['owner_photo'] === null\n ? null\n : client_cdn($row['owner_photo'], $team),\n 'id' => $row['owner_id'],\n 'job' => $row['owner_job'],\n ];\n }\n\n $result[(int) $row['opp_id']] = $data;\n }\n\n return $this->hydrateStages($result);\n }\n\n private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder\n {\n $qb = clone $queryBuilder;\n $qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');\n\n $qb\n ->select(...[\n 'opp.id as opp_id',\n 'opp.uuid',\n 'opp.name',\n 'opp.value',\n 'opp.currency_code',\n 'opp.close_date',\n 'opp.remotely_created_at',\n 'opp.is_closed',\n 'opp.is_won',\n ])\n ->addSelect(...[\n 'usr.uuid as owner_uuid',\n 'usr.name AS owner_name',\n 'usr.photo_path as owner_photo',\n 'usr.id AS owner_id',\n 'jt.name as owner_job',\n ])\n ->addSelect('opp.stage_id', 'opp.stage_updated_at')\n ->addSelect(...[\n 'acc.name AS acc_name',\n 'acc.is_internal as acc_is_internal',\n 'opp.stage_updated_at',\n 'acc.crm_provider_id AS acc_provider_id',\n 'opp.crm_provider_id AS opp_provider_id',\n ])\n ->addSelect('rt.business_process_id AS pipeline_id')\n\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'));\n\n return $qb;\n }\n\n /**\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws SocialAccountTokenInvalidException\n */\n private function getCrmService(Team $team): ServiceInterface\n {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n $crmService->setConfiguration($team->crm);\n if ($crmService instanceof UrlGeneratorInterface) {\n $crmService->setCrmUrlGenerator($team->crm);\n }\n\n return $crmService;\n }\n\n /**\n *\n * @return Generator<DealData>\n */\n public function getForecastData(DealsFilter $filter): Generator\n {\n $opportunities = DB::query()\n ->select([\n 'o.value',\n 'o.close_date',\n 'o.currency_code',\n 'o.is_won',\n 'o.is_closed',\n 'o.probability',\n 'o.forecast_category',\n ])\n ->from('opportunities', 'o')\n ->join('users', 'users.id', '=', 'o.user_id')\n ->join('groups', 'groups.id', '=', 'users.group_id')\n ->where('users.team_id', $filter->getTeam()->getId())\n ->where('o.close_date', '>=', $filter->getStartDate())\n ->where('o.close_date', '<=', $filter->getEndDate())\n ->where('o.currency_code', $filter->getCurrency())\n ->where('o.deleted_at', '=', null)\n ;\n\n $userUuidList = $filter->getUserUuidList();\n if (! empty($userUuidList)) {\n $userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);\n\n $opportunities->whereIn('users.uuid', $userUuidList);\n }\n\n $groupUuidList = $filter->getGroupUuidList();\n if (! empty($groupUuidList)) {\n $groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);\n\n $opportunities->whereIn('groups.uuid', $groupUuidList);\n }\n\n foreach ($opportunities->cursor() as $row) {\n yield new DealData(\n (float) $row->value,\n $row->close_date,\n ! empty($row->is_won),\n ! empty($row->is_closed),\n $row->probability ?: 0,\n $row->forecast_category ?: '',\n );\n }\n }\n\n public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection\n {\n return $user->subscriptionSets()\n ->where(static function (Eloquent\\Builder $query): void {\n $query\n ->whereNull('expired_at')\n ->orWhere('expired_at', '>=', now());\n })\n ->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n $join\n ->where('followable_type', Models\\Activity\\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)\n ->whereIn('followable_id', $opportunityIds);\n })\n ->pluck('followable_id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\DealInsights;\n\nuse Doctrine\\DBAL\\Connection;\nuse Generator;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealData;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealsFilter;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\QueryBuilder;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\Visitor\\QueryBuilderVisitorInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Services\\Crm\\IntegrationApp\\DTO\\Utils\\UrlGeneratorInterface;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Eloquent;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass DealsRepository implements DealsRepositoryInterface\n{\n private Connection $connection;\n\n private ProviderRegistry $providerRegistry;\n\n /**\n * @var QueryBuilderVisitorInterface[]\n */\n private array $visitors = [];\n\n /**\n * @param QueryBuilderVisitorInterface[] $visitors\n */\n public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])\n {\n $this->connection = $connection;\n $this->providerRegistry = $crmProviderRegistry;\n\n foreach ($visitors as $visitor) {\n $this->visitors[$visitor->getIdentifier()] = $visitor;\n }\n }\n\n public function getDeals(CriteriaInterface $criteria): array\n {\n $context = $criteria->getContext();\n $team = $context->getTeam();\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n\n $this->visit($qb, $criteria);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getDeal(Team $team, int $id): array\n {\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n $qb->andWhere('opp.id = :id')->setParameter('id', $id);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')\n ->from('crm_fields', 'f')\n ->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')\n ->where('f.crm_configuration_id = :crm')\n ->andWhere('f.object_type = :type')\n ->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')\n ->orderBy('fd.object_id', 'ASC')\n ->addOrderBy('fd.updated_at', 'ASC')\n\n ->setParameter('type', Field::OBJECT_OPPORTUNITY)\n ->setParameter('crm', $crmId)\n ;\n\n if (! empty($crmFields)) {\n $fields = array_map(fn ($value): string => '\"' . $value . '\"', $crmFields);\n $qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');\n }\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAssociative();\n }\n\n public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('COALESCE(opp.currency_code, \"' . $defaultCurrency . '\") AS currency')\n ->addSelect('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ->groupBy('currency')\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getDealActivities(CriteriaInterface $criteria): array\n {\n $qb = Activity::with(['participants', 'user'])\n ->where('opportunity_id', $criteria->getOpportunityId())\n ->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())\n ->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())\n ->orderBy($criteria->getSortBy(), $criteria->getSortDirection())\n ;\n\n // Should we filter activities by criteria? It's intended to filter deals.\n\n return $qb->get()->all();\n }\n\n public function getStages(CriteriaInterface $criteria): array\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('id', 'label', 'sequence')\n ->from('stages', 's')\n ->where('crm_configuration_id = :crm_configuration_id')\n ->andWhere('type = :type')\n ->orderBy('sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())\n ->setParameter('type', Stage::TYPE_OPPORTUNITY);\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $result[$row['id']] = [\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n public function getConfigurationStages(Configuration $configuration): Collection\n {\n return $configuration\n ->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get();\n }\n\n public function getPipelineData(Configuration $crm): array\n {\n $qb = new QueryBuilder($this->connection);\n $provider = $crm->provider;\n\n $qb\n ->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')\n ->from('stages', 's')\n ->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')\n ->where('s.crm_configuration_id = :crm_configuration_id')\n ->andWhere('s.type = :type')\n ->orderBy('bps.business_process_id', 'ASC')\n ->addOrderBy('s.sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $crm->id)\n ->setParameter('type', Stage::TYPE_OPPORTUNITY)\n ;\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];\n $result[$row['pipeline_id']][] = [\n 'value' => $value,\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n private function createQueryBuilder(string $realm): QueryBuilder\n {\n return (new QueryBuilder($this->connection))\n ->setRealm($realm)\n ->from('opportunities', 'opp')\n ->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')\n ->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')\n ->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')\n ;\n }\n\n /**\n * Applies all applicable visitors and returns the IDs of the executed ones\n *\n * @return string[]\n */\n private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array\n {\n $queryVisitors = [];\n\n foreach ($this->visitors as $visitor) {\n if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {\n $visitor->visit($queryBuilder, $criteria);\n\n $queryVisitors[] = $visitor->getIdentifier();\n }\n }\n\n return $queryVisitors;\n }\n\n private function hydrateStages(array $deals): array\n {\n foreach ($this->fetchStages(array_keys($deals)) as $stage) {\n $oppId = (int) $stage['opportunity_id'];\n\n if (! isset($deals[$oppId])) {\n continue; // or throw??!\n }\n\n $deals[$oppId]['stages'][] = [\n 'id' => $stage['stage_id'],\n 'name' => $stage['label'],\n 'enteredAt' => $stage['created_at'],\n ];\n }\n\n return $deals;\n }\n\n /**\n * @param int[] $dealIds\n */\n private function fetchStages(array $dealIds): array\n {\n if (empty($dealIds)) {\n return [];\n }\n\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')\n ->from('opportunity_stages', 'os')\n ->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')\n ->where($qb->expr()->in('os.opportunity_id', $dealIds))\n ->orderBy('os.opportunity_id', 'ASC')\n ->addOrderBy('s.created_at', 'ASC')\n ;\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array\n {\n $result = [];\n\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $data = [\n 'uuid' => RequiresUUID::toNormal($row['uuid']),\n 'name' => $row['name'],\n 'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),\n 'account' => [\n 'name' => $row['acc_name'],\n 'url' => $crmService->generateProviderUrl(\n providerId: $row['acc_provider_id'],\n objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'\n ),\n ],\n 'owner' => null,\n 'rawValue' => [\n 'amount' => (float) $row['value'],\n 'currency' => $row['currency_code'],\n ],\n 'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),\n 'openDate' => $row['remotely_created_at'] ?? null,\n 'closeDate' => $row['close_date'] ?? null,\n 'stages' => [],\n 'currentPipelineId' => $row['pipeline_id'],\n 'currentStage' => [\n 'id' => $row['stage_id'],\n 'enteredAt' => $row['stage_updated_at'],\n ],\n 'currentStageUpdatedAt' => $row['stage_updated_at'],\n 'isClosed' => (bool) $row['is_closed'],\n 'isWon' => (bool) $row['is_won'],\n ];\n\n if (isset($row['owner_uuid'])) {\n $data['owner'] = [\n 'uuid' => RequiresUUID::toNormal($row['owner_uuid']),\n 'name' => $row['owner_name'],\n 'photoUrl' => $row['owner_photo'] === null\n ? null\n : client_cdn($row['owner_photo'], $team),\n 'id' => $row['owner_id'],\n 'job' => $row['owner_job'],\n ];\n }\n\n $result[(int) $row['opp_id']] = $data;\n }\n\n return $this->hydrateStages($result);\n }\n\n private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder\n {\n $qb = clone $queryBuilder;\n $qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');\n\n $qb\n ->select(...[\n 'opp.id as opp_id',\n 'opp.uuid',\n 'opp.name',\n 'opp.value',\n 'opp.currency_code',\n 'opp.close_date',\n 'opp.remotely_created_at',\n 'opp.is_closed',\n 'opp.is_won',\n ])\n ->addSelect(...[\n 'usr.uuid as owner_uuid',\n 'usr.name AS owner_name',\n 'usr.photo_path as owner_photo',\n 'usr.id AS owner_id',\n 'jt.name as owner_job',\n ])\n ->addSelect('opp.stage_id', 'opp.stage_updated_at')\n ->addSelect(...[\n 'acc.name AS acc_name',\n 'acc.is_internal as acc_is_internal',\n 'opp.stage_updated_at',\n 'acc.crm_provider_id AS acc_provider_id',\n 'opp.crm_provider_id AS opp_provider_id',\n ])\n ->addSelect('rt.business_process_id AS pipeline_id')\n\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'));\n\n return $qb;\n }\n\n /**\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws SocialAccountTokenInvalidException\n */\n private function getCrmService(Team $team): ServiceInterface\n {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n $crmService->setConfiguration($team->crm);\n if ($crmService instanceof UrlGeneratorInterface) {\n $crmService->setCrmUrlGenerator($team->crm);\n }\n\n return $crmService;\n }\n\n /**\n *\n * @return Generator<DealData>\n */\n public function getForecastData(DealsFilter $filter): Generator\n {\n $opportunities = DB::query()\n ->select([\n 'o.value',\n 'o.close_date',\n 'o.currency_code',\n 'o.is_won',\n 'o.is_closed',\n 'o.probability',\n 'o.forecast_category',\n ])\n ->from('opportunities', 'o')\n ->join('users', 'users.id', '=', 'o.user_id')\n ->join('groups', 'groups.id', '=', 'users.group_id')\n ->where('users.team_id', $filter->getTeam()->getId())\n ->where('o.close_date', '>=', $filter->getStartDate())\n ->where('o.close_date', '<=', $filter->getEndDate())\n ->where('o.currency_code', $filter->getCurrency())\n ->where('o.deleted_at', '=', null)\n ;\n\n $userUuidList = $filter->getUserUuidList();\n if (! empty($userUuidList)) {\n $userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);\n\n $opportunities->whereIn('users.uuid', $userUuidList);\n }\n\n $groupUuidList = $filter->getGroupUuidList();\n if (! empty($groupUuidList)) {\n $groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);\n\n $opportunities->whereIn('groups.uuid', $groupUuidList);\n }\n\n foreach ($opportunities->cursor() as $row) {\n yield new DealData(\n (float) $row->value,\n $row->close_date,\n ! empty($row->is_won),\n ! empty($row->is_closed),\n $row->probability ?: 0,\n $row->forecast_category ?: '',\n );\n }\n }\n\n public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection\n {\n return $user->subscriptionSets()\n ->where(static function (Eloquent\\Builder $query): void {\n $query\n ->whereNull('expired_at')\n ->orWhere('expired_at', '>=', now());\n })\n ->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n $join\n ->where('followable_type', Models\\Activity\\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)\n ->whereIn('followable_id', $opportunityIds);\n })\n ->pluck('followable_id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
6727517982887829105
|
7071332928453159851
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
111
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands;
use Carbon\Carbon;
use Illuminate\Console\Command;
use InvalidArgumentException;
use Jiminny\Jobs\AutomatedReports\RequestGenerateAskJiminnyReportJob;
use Jiminny\Jobs\AutomatedReports\SendReportMailJob;
use Jiminny\Jobs\JobDispatcherInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
/**
* Class JiminnyDebugCommand
*
* @package Jiminny\Console\Commands
*/
class JiminnyDebugCommand extends Command
{
public const string FREQUENCY_DAILY = 'daily';
public const string FREQUENCY_WEEKLY = 'weekly';
public const string FREQUENCY_MONTHLY = 'monthly';
public const string FREQUENCY_QUARTERLY = 'quarterly';
public const string FREQUENCY_ONE_OFF = 'one_off';
protected $signature = 'jiminny:debug';
public function handle(JobDispatcherInterface $jobDispatcher, AutomatedReportsService $automatedReportsService): void
{
exit(1);
$now = Carbon::now()->subDay(5);
$from = $now->copy()->previousWeekday()->startOfDay();
$to = $now->copy()->previousWeekday()->endOfDay();
$this->info("Now: {$now->toDateTimeString()}");
$this->info("From: {$from->toDateTimeString()}");
$this->info("To: {$to->toDateTimeString()}");
exit(1);
$report = AutomatedReport::find(71);
$job = new RequestGenerateAskJiminnyReportJob($report->getUuid());
$jobDispatcher->dispatch($job);
exit(1);
// $this->formatDate($jobDispatcher);
// $this->sendMail($jobDispatcher, $automatedReportsService);
// $this->crmService();
$this->getPayload($automatedReportsService);
exit(1);
}
private function crmService()
{
$activity = Activity::find(418141);
$team = Team::find(19);
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
$crmService->createTranscriptNotes($activity);
}
private function sendMail(JobDispatcherInterface $jobDispatcher, AutomatedReportsService $automatedReportsService)
{
$reportUuid = '';
// $report = $automatedReportsService->getReportResult($reportUuid);
$report = AutomatedReportResult::find(275);
$validRecipients = $automatedReportsService->getValidRecipientUsers(
$report->getReport(),
includeJiminny: true,
);
$recipient = $validRecipients[0];
$fileName = $automatedReportsService->getReportFileName($report);
$typeName = $report->getReport()->getCustomName()
?? $automatedReportsService->getReportTypeName($report);
$teamsName = $automatedReportsService->getReportTeamsName($report);
$periodName = $automatedReportsService->getReportPeriodName($report);
$s3Path = $automatedReportsService->getMediaPath($report);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$fileName ' . PHP_EOL . print_r($fileName, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$typeName ' . PHP_EOL . print_r($typeName, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$teamsName ' . PHP_EOL . print_r($teamsName, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$periodName ' . PHP_EOL . print_r($periodName, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$s3Path ' . PHP_EOL . print_r($s3Path, true));
$jobDispatcher->dispatch(
new SendReportMailJob(
reportUuid: $report->getUuid(),
s3Path: $s3Path,
recipientEmail: $recipient['email'],
recipientName: $recipient['name'] ?? null,
fileName: $fileName,
typeName: $typeName,
teamsName: $teamsName,
periodName: $periodName,
isAskJiminny: true,
)
);
exit(1);
}
private function formatDate(JobDispatcherInterface $jobDispatcher): void
{
$customName = 'Custom report name';
// $frequency = self::FREQUENCY_DAILY;
// $frequency = self::FREQUENCY_WEEKLY;
$frequency = self::FREQUENCY_MONTHLY;
// $frequency = self::FREQUENCY_QUARTERLY;
// $frequency = self::FREQUENCY_ONE_OFF;
$period = $this->calculateFromAndToDatePeriod($frequency);
$from = $period['fromDate'];
$to = $period['toDate'];
$periodName = $this->formatReportPeriodName($frequency, $from, $to);
$filenameSuffix = null;
if ($customName) {
if ($filenameSuffix) {
$customName .= " {$filenameSuffix}";
}
$result = $this->sanitizeFileName("{$customName} - {$periodName}");
}
$this->info($result);
}
public function calculateFromAndToDatePeriod(
string $frequency,
?Carbon $fromDate = null,
?Carbon $toDate = null
): array {
if ($frequency === self::FREQUENCY_ONE_OFF) {
return [
'fromDate' => $fromDate,
'toDate' => $toDate,
];
}
$now = Carbon::now();
return match ($frequency) {
self::FREQUENCY_DAILY => [
'fromDate' => $now->copy()->subDay()->startOfDay(),
'toDate' => $now->copy()->subDay()->endOfDay(),
],
self::FREQUENCY_WEEKLY => [
'fromDate' => $now->copy()->subWeeks(1)->startOfDay(),
'toDate' => $now->copy()->subDay()->endOfDay(),
],
self::FREQUENCY_MONTHLY => [
'fromDate' => $now->copy()->subMonths(1)->startOfDay(),
'toDate' => $now->copy()->subDay()->endOfDay(),
],
self::FREQUENCY_QUARTERLY => [
'fromDate' => $now->copy()->subMonths(3)->startOfDay(),
'toDate' => $now->copy()->subDay()->endOfDay(),
],
default => throw new InvalidArgumentException("Unsupported frequency: {$frequency}"),
};
}
private function formatReportPeriodName(string $frequency, Carbon $from, Carbon $to): string
{
$fromYear = $from->format('Y');
$toYear = $to->format('Y');
$differentYears = $fromYear !== $toYear;
switch ($frequency) {
case self::FREQUENCY_DAILY:
return $from->format('j M Y');
case self::FREQUENCY_QUARTERLY:
// 'Jan-Mar 2025' or 'Nov 2024-Jan 2025' if years differ
$startMonth = $from->format('M');
$endMonth = $to->copy()->subMonth();
$endMonthName = $endMonth->format('M');
$endMonthYear = $endMonth->format('Y');
if ($differentYears) {
return "{$startMonth} {$fromYear} - {$endMonthName} {$endMonthYear}";
}
return "{$startMonth} - {$endMonthName} {$toYear}";
case self::FREQUENCY_MONTHLY:
// 'May 2025' - monthly reports are always within the same year
return $from->format('M Y');
case self::FREQUENCY_WEEKLY:
// '4 - 8 Aug 2025', '27 Oct - 3 Nov 2025', or '28 Dec 2024 - 3 Jan 2025' if years differ
$startDay = $from->format('j');
$endDay = $to->format('j');
$startMonth = $from->format('M');
$endMonth = $to->format('M');
if ($differentYears) {
return "{$startDay} {$startMonth} {$fromYear} - {$endDay} {$endMonth} {$toYear}";
}
if ($startMonth !== $endMonth) {
return "{$startDay} {$startMonth} - {$endDay} {$endMonth} {$toYear}";
}
return "{$startDay} - {$endDay} {$endMonth} {$toYear}";
case self::FREQUENCY_ONE_OFF:
// '2 May-31 May 2025' or '15 Dec 2024-15 Jan 2025' if years differ
$startDay = $from->format('j');
$startMonth = $from->format('M');
$endDay = $to->format('j');
$endMonth = $to->format('M');
// If same month and year, use a format like '2-31 May 2025'
if ($startMonth === $endMonth && ! $differentYears) {
return "{$startDay} - {$endDay} {$startMonth} {$toYear}";
}
// If different years, include both years
if ($differentYears) {
return "{$startDay} {$startMonth} {$fromYear} - {$endDay} {$endMonth} {$toYear}";
}
// Same year but different months
return "{$startDay} {$startMonth} - {$endDay} {$endMonth} {$toYear}";
default:
// Default format for unknown frequencies
return $from->format('j M Y') . ' - ' . $to->format('j M Y');
}
}
public function sanitizeFileName(string $fileName): string
{
return str_replace(['/', '\\'], '-', $fileName);
}
private function getPayload(AutomatedReportsService $automatedReportsService)
{
$reportResult = AutomatedReportResult::find(269);
$automatedReport = $reportResult->getReport();
$activityIds = [1,2,3];
$payload = $automatedReportsService->getAskJiminnyGenerateReportPayload(
automatedReport: $automatedReport,
reportResult: $reportResult,
activityIds: $activityIds,
);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$payload ' . PHP_EOL . print_r($payload, true));
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
36
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\DealInsights;
use Doctrine\DBAL\Connection;
use Generator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Jiminny\Component\DealInsights\Forecast\DealData;
use Jiminny\Component\DealInsights\Forecast\DealsFilter;
use Jiminny\Component\DealInsights\QueryBuilder\QueryBuilder;
use Jiminny\Component\DealInsights\QueryBuilder\Visitor\QueryBuilderVisitorInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Services\Crm\IntegrationApp\DTO\Utils\UrlGeneratorInterface;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class DealsRepository implements DealsRepositoryInterface
{
private Connection $connection;
private ProviderRegistry $providerRegistry;
/**
* @var QueryBuilderVisitorInterface[]
*/
private array $visitors = [];
/**
* @param QueryBuilderVisitorInterface[] $visitors
*/
public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])
{
$this->connection = $connection;
$this->providerRegistry = $crmProviderRegistry;
foreach ($visitors as $visitor) {
$this->visitors[$visitor->getIdentifier()] = $visitor;
}
}
public function getDeals(CriteriaInterface $criteria): array
{
$context = $criteria->getContext();
$team = $context->getTeam();
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$this->visit($qb, $criteria);
return $this->execute($team, $crmService, $qb);
}
public function getDeal(Team $team, int $id): array
{
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$qb->andWhere('opp.id = :id')->setParameter('id', $id);
return $this->execute($team, $crmService, $qb);
}
public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])
{
$qb = new QueryBuilder($this->connection);
$qb
->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')
->from('crm_fields', 'f')
->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')
->where('f.crm_configuration_id = :crm')
->andWhere('f.object_type = :type')
->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')
->orderBy('fd.object_id', 'ASC')
->addOrderBy('fd.updated_at', 'ASC')
->setParameter('type', Field::OBJECT_OPPORTUNITY)
->setParameter('crm', $crmId)
;
if (! empty($crmFields)) {
$fields = array_map(fn ($value): string => '"' . $value . '"', $crmFields);
$qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');
}
return $qb->executeQuery()->fetchAllAssociative();
}
public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAssociative();
}
public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('COALESCE(opp.currency_code, "' . $defaultCurrency . '") AS currency')
->addSelect('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
->groupBy('currency')
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAllAssociative();
}
public function getDealActivities(CriteriaInterface $criteria): array
{
$qb = Activity::with(['participants', 'user'])
->where('opportunity_id', $criteria->getOpportunityId())
->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())
->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())
->orderBy($criteria->getSortBy(), $criteria->getSortDirection())
;
// Should we filter activities by criteria? It's intended to filter deals.
return $qb->get()->all();
}
public function getStages(CriteriaInterface $criteria): array
{
$qb = new QueryBuilder($this->connection);
$qb
->select('id', 'label', 'sequence')
->from('stages', 's')
->where('crm_configuration_id = :crm_configuration_id')
->andWhere('type = :type')
->orderBy('sequence', 'ASC')
->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())
->setParameter('type', Stage::TYPE_OPPORTUNITY);
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$result[$row['id']] = [
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
public function getConfigurationStages(Configuration $configuration): Collection
{
return $configuration
->stages()
->where('type', Stage::TYPE_OPPORTUNITY)
->get();
}
public function getPipelineData(Configuration $crm): array
{
$qb = new QueryBuilder($this->connection);
$provider = $crm->provider;
$qb
->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')
->from('stages', 's')
->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')
->where('s.crm_configuration_id = :crm_configuration_id')
->andWhere('s.type = :type')
->orderBy('bps.business_process_id', 'ASC')
->addOrderBy('s.sequence', 'ASC')
->setParameter('crm_configuration_id', $crm->id)
->setParameter('type', Stage::TYPE_OPPORTUNITY)
;
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];
$result[$row['pipeline_id']][] = [
'value' => $value,
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
private function createQueryBuilder(string $realm): QueryBuilder
{
return (new QueryBuilder($this->connection))
->setRealm($realm)
->from('opportunities', 'opp')
->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')
->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')
->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')
;
}
/**
* Applies all applicable visitors and returns the IDs of the executed ones
*
* @return string[]
*/
private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array
{
$queryVisitors = [];
foreach ($this->visitors as $visitor) {
if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {
$visitor->visit($queryBuilder, $criteria);
$queryVisitors[] = $visitor->getIdentifier();
}
}
return $queryVisitors;
}
private function hydrateStages(array $deals): array
{
foreach ($this->fetchStages(array_keys($deals)) as $stage) {
$oppId = (int) $stage['opportunity_id'];
if (! isset($deals[$oppId])) {
continue; // or throw??!
}
$deals[$oppId]['stages'][] = [
'id' => $stage['stage_id'],
'name' => $stage['label'],
'enteredAt' => $stage['created_at'],
];
}
return $deals;
}
/**
* @param int[] $dealIds
*/
private function fetchStages(array $dealIds): array
{
if (empty($dealIds)) {
return [];
}
$qb = new QueryBuilder($this->connection);
$qb
->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')
->from('opportunity_stages', 'os')
->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')
->where($qb->expr()->in('os.opportunity_id', $dealIds))
->orderBy('os.opportunity_id', 'ASC')
->addOrderBy('s.created_at', 'ASC')
;
return $qb->executeQuery()->fetchAllAssociative();
}
private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array
{
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$data = [
'uuid' => RequiresUUID::toNormal($row['uuid']),
'name' => $row['name'],
'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),
'account' => [
'name' => $row['acc_name'],
'url' => $crmService->generateProviderUrl(
providerId: $row['acc_provider_id'],
objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'
),
],
'owner' => null,
'rawValue' => [
'amount' => (float) $row['value'],
'currency' => $row['currency_code'],
],
'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),
'openDate' => $row['remotely_created_at'] ?? null,
'closeDate' => $row['close_date'] ?? null,
'stages' => [],
'currentPipelineId' => $row['pipeline_id'],
'currentStage' => [
'id' => $row['stage_id'],
'enteredAt' => $row['stage_updated_at'],
],
'currentStageUpdatedAt' => $row['stage_updated_at'],
'isClosed' => (bool) $row['is_closed'],
'isWon' => (bool) $row['is_won'],
];
if (isset($row['owner_uuid'])) {
$data['owner'] = [
'uuid' => RequiresUUID::toNormal($row['owner_uuid']),
'name' => $row['owner_name'],
'photoUrl' => $row['owner_photo'] === null
? null
: client_cdn($row['owner_photo'], $team),
'id' => $row['owner_id'],
'job' => $row['owner_job'],
];
}
$result[(int) $row['opp_id']] = $data;
}
return $this->hydrateStages($result);
}
private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder
{
$qb = clone $queryBuilder;
$qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');
$qb
->select(...[
'opp.id as opp_id',
'opp.uuid',
'opp.name',
'opp.value',
'opp.currency_code',
'opp.close_date',
'opp.remotely_created_at',
'opp.is_closed',
'opp.is_won',
])
->addSelect(...[
'usr.uuid as owner_uuid',
'usr.name AS owner_name',
'usr.photo_path as owner_photo',
'usr.id AS owner_id',
'jt.name as owner_job',
])
->addSelect('opp.stage_id', 'opp.stage_updated_at')
->addSelect(...[
'acc.name AS acc_name',
'acc.is_internal as acc_is_internal',
'opp.stage_updated_at',
'acc.crm_provider_id AS acc_provider_id',
'opp.crm_provider_id AS opp_provider_id',
])
->addSelect('rt.business_process_id AS pipeline_id')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'));
return $qb;
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws SocialAccountTokenInvalidException
*/
private function getCrmService(Team $team): ServiceInterface
{
$crmService = $this->providerRegistry->get($team->crm->provider);
$crmService->setConfiguration($team->crm);
if ($crmService instanceof UrlGeneratorInterface) {
$crmService->setCrmUrlGenerator($team->crm);
}
return $crmService;
}
/**
*
* @return Generator<DealData>
*/
public function getForecastData(DealsFilter $filter): Generator
{
$opportunities = DB::query()
->select([
'o.value',
'o.close_date',
'o.currency_code',
'o.is_won',
'o.is_closed',
'o.probability',
'o.forecast_category',
])
->from('opportunities', 'o')
->join('users', 'users.id', '=', 'o.user_id')
->join('groups', 'groups.id', '=', 'users.group_id')
->where('users.team_id', $filter->getTeam()->getId())
->where('o.close_date', '>=', $filter->getStartDate())
->where('o.close_date', '<=', $filter->getEndDate())
->where('o.currency_code', $filter->getCurrency())
->where('o.deleted_at', '=', null)
;
$userUuidList = $filter->getUserUuidList();
if (! empty($userUuidList)) {
$userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);
$opportunities->whereIn('users.uuid', $userUuidList);
}
$groupUuidList = $filter->getGroupUuidList();
if (! empty($groupUuidList)) {
$groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);
$opportunities->whereIn('groups.uuid', $groupUuidList);
}
foreach ($opportunities->cursor() as $row) {
yield new DealData(
(float) $row->value,
$row->close_date,
! empty($row->is_won),
! empty($row->is_closed),
$row->probability ?: 0,
$row->forecast_category ?: '',
);
}
}
public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection
{
return $user->subscriptionSets()
->where(static function (Eloquent\Builder $query): void {
$query
->whereNull('expired_at')
->orWhere('expired_at', '>=', now());
})
->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {
$join
->on('subscription_set_id', '=', 'activity_subscription_sets.id');
$join
->where('followable_type', Models\Activity\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)
->whereIn('followable_id', $opportunityIds);
})
->pluck('followable_id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
73511
|
|
73473
|
NULL
|
0
|
2026-04-23T07:46:37.128791+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776930397128_m1.jpg...
|
Slack
|
Aneliya Angelova, Nikolay Yankov, Steliyan Georgie Aneliya Angelova, Nikolay Yankov, Steliyan Georgiev (DM) - Jiminny Inc - 1 new item - Slack...
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
c-learning-people
confusion-clinic
curiosity_lab
deal-insights-dev
engineering
frontend
general
infra-changes
jiminny-bg
people-with-copilot-licences
people-with-zoom-phone-licences
platform-team
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Stoyan Tomov
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Nikolay Yankov
Petko Kashinski
Aneliya Angelova
Nikolay Nikolov
Mario Georgiev
Todor Stamatov
Gabriela Dureva
Vasil Vasilev
Galya Dimitrova
Stefka Stoyanova
Stoyan Tanev
Nikolay Ivanov
Toast
Jira Cloud
Messages
Messages
Add canvas
Add canvas
Files
Files
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Aneliya Angelova
Today at 10:08:44 AM
10:08 AM
за да мимикира точно както е автоматичното генериране на репорта - може ли да е за фиксирана седмица назад
Today at 10:08:57 AM
10:08
филтъра и това което се изписва във пдфа
Lukas Kovalik
Today at 10:09:09 AM
10:09 AM
и аз се чудих, то за това с получи разминаване
Today at 10:10:26 AM
10:10
PDF мисля че беше 7 дни на зад и activity бяха от понеделник до неделя
Today at 10:10:46 AM
10:10
реално ако се пуска от крон йоб трябва да е ок, само ръчно
Aneliya Angelova
Today at 10:10:53 AM
10:10 AM
да
Lukas Kovalik
Today at 10:11:02 AM
10:11 AM
направих го сега 7 дни на всякъде
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Aneliya Angelova
Today at 10:11:03 AM
10:11 AM
и на двете места да стане понеделник-неделя
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 10:11:46 AM
10:11 AM
но реално ако искаме ретроспективно да пуснем нещо седмично което не е минало в сряда ще е от среяда до четвъртък
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Aneliya Angelova
Today at 10:13:21 AM
10:13 AM
Галя казва, че по скоро иска винаги да се пускат от понеделник до неделя, за да може да има бекъп вариант ако трябва по някаква причина ръчно да се рерънне седмичен репорт на клиент
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 10:13:47 AM
10:13 AM
да, ами добре ще ги сменя, на старите репорти
(edited)
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Aneliya Angelova
Today at 10:14:00 AM
10:14 AM
оки
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 10:15:09 AM
10:15
тайтъла на репорта и Data Source секцията ще показват правилния период нали - понеже това се разминаваше до сега
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
сега забелязах че стария count който се ползва в при контрол на достъп цанАццессАиРепор
сега забелязах че стария count който се ползва в при контрол на достъп цанАццессАиРепор
Shift + Return to add a new line
Shift + Return
to add a new line
Conversation marked as unread from this message
Channel...
|
[{"role":"AXPopUpButton","text [{"role":"AXPopUpButton","text":"Switch workspaces… (Jiminny Inc) Has new messages","depth":14,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"Home","depth":14,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Home","depth":16,"role_description":"text"},{"role":"AXRadioButton","text":"DMs","depth":14,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"DMs","depth":16,"role_description":"text"},{"role":"AXRadioButton","text":"Activity","depth":14,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Activity","depth":16,"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":14,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":16,"role_description":"text"},{"role":"AXRadioButton","text":"Later","depth":14,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Later","depth":16,"role_description":"text"},{"role":"AXRadioButton","text":"More…","depth":14,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More","depth":16,"role_description":"text"},{"role":"AXStaticText","text":"Unreads","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Threads","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Huddles","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Drafts & sent","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Directories","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"jiminny-x-integration-app","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"platform-inner-team","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"ai-chapter","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"alerts","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"backend","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"c-learning-people","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"confusion-clinic","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"curiosity_lab","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"deal-insights-dev","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"engineering","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"frontend","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"general","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"infra-changes","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"jiminny-bg","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"people-with-copilot-licences","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"people-with-zoom-phone-licences","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"platform-team","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"platform-tickets","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"product_launches","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"random","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"releases","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"sofia-office","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"support","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"thank-yous","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"the_people_of_jiminny","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Stoyan Tomov","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":23,"role_description":"text"},{"role":"AXStaticText","text":",","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Yankov","depth":23,"role_description":"text"},{"role":"AXStaticText","text":",","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Steliyan Georgiev","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Yankov","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Petko Kashinski","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Nikolov","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Mario Georgiev","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Todor Stamatov","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Gabriela Dureva","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Vasil Vasilev","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Galya Dimitrova","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Stefka Stoyanova","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Stoyan Tanev","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Ivanov","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Toast","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Jira Cloud","depth":23,"role_description":"text"},{"role":"AXRadioButton","text":"Messages","depth":17,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Messages","depth":19,"role_description":"text"},{"role":"AXRadioButton","text":"Add canvas","depth":18,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Add canvas","depth":20,"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":17,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":19,"role_description":"text"},{"role":"AXPopUpButton","text":"Add and Edit Channel Tabs","depth":17,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Canvas","depth":17,"role_description":"text"},{"role":"AXStaticText","text":"List","depth":17,"role_description":"text"},{"role":"AXStaticText","text":"Folder","depth":17,"role_description":"text"},{"role":"AXPopUpButton","text":"Jump to date","depth":23,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Aneliya Angelova","depth":24,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"role_description":"text"},{"role":"AXLink","text":"Today at 10:08:44 AM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:08 AM","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"за да мимикира точно както е автоматичното генериране на репорта - може ли да е за фиксирана седмица назад","depth":25,"role_description":"text"},{"role":"AXLink","text":"Today at 10:08:57 AM","depth":25,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:08","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"филтъра и това което се изписва във пдфа","depth":25,"role_description":"text"},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"role_description":"text"},{"role":"AXLink","text":"Today at 10:09:09 AM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:09 AM","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"и аз се чудих, то за това с получи разминаване","depth":25,"role_description":"text"},{"role":"AXLink","text":"Today at 10:10:26 AM","depth":25,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:10","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"PDF мисля че беше 7 дни на зад и activity бяха от понеделник до неделя","depth":25,"role_description":"text"},{"role":"AXLink","text":"Today at 10:10:46 AM","depth":25,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:10","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"реално ако се пуска от крон йоб трябва да е ок, само ръчно","depth":25,"role_description":"text"},{"role":"AXButton","text":"Aneliya Angelova","depth":24,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"role_description":"text"},{"role":"AXLink","text":"Today at 10:10:53 AM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:10 AM","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"да","depth":25,"role_description":"text"},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"role_description":"text"},{"role":"AXLink","text":"Today at 10:11:02 AM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:11 AM","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"направих го сега 7 дни на всякъде","depth":25,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Aneliya Angelova","depth":24,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"role_description":"text"},{"role":"AXLink","text":"Today at 10:11:03 AM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:11 AM","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"и на двете места да стане понеделник-неделя","depth":25,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"role_description":"text"},{"role":"AXLink","text":"Today at 10:11:46 AM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:11 AM","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"но реално ако искаме ретроспективно да пуснем нещо седмично което не е минало в сряда ще е от среяда до четвъртък","depth":25,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Aneliya Angelova","depth":24,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"role_description":"text"},{"role":"AXLink","text":"Today at 10:13:21 AM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:13 AM","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"Галя казва, че по скоро иска винаги да се пускат от понеделник до неделя, за да може да има бекъп вариант ако трябва по някаква причина ръчно да се рерънне седмичен репорт на клиент","depth":25,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"role_description":"text"},{"role":"AXLink","text":"Today at 10:13:47 AM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:13 AM","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"да, ами добре ще ги сменя, на старите репорти","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"(edited)","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Aneliya Angelova","depth":24,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"role_description":"text"},{"role":"AXLink","text":"Today at 10:14:00 AM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:14 AM","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"оки","depth":25,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 10:15:09 AM","depth":25,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:15","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"тайтъла на репорта и Data Source секцията ще показват правилния период нали - понеже това се разминаваше до сега","depth":25,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"сега забелязах че стария count който се ползва в при контрол на достъп цанАццессАиРепор","depth":23,"value":"сега забелязах че стария count който се ползва в при контрол на достъп цанАццессАиРепор","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"сега забелязах че стария count който се ползва в при контрол на достъп цанАццессАиРепор","depth":25,"role_description":"text"},{"role":"AXButton","text":"Shift + Return to add a new line","depth":20,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Shift + Return","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"to add a new line","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Conversation marked as unread from this message","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"Channel","depth":11,"role_description":"text"}]...
|
-1611057946642660973
|
-1280970121134471104
|
idle
|
hybrid
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
c-learning-people
confusion-clinic
curiosity_lab
deal-insights-dev
engineering
frontend
general
infra-changes
jiminny-bg
people-with-copilot-licences
people-with-zoom-phone-licences
platform-team
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Stoyan Tomov
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Nikolay Yankov
Petko Kashinski
Aneliya Angelova
Nikolay Nikolov
Mario Georgiev
Todor Stamatov
Gabriela Dureva
Vasil Vasilev
Galya Dimitrova
Stefka Stoyanova
Stoyan Tanev
Nikolay Ivanov
Toast
Jira Cloud
Messages
Messages
Add canvas
Add canvas
Files
Files
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Aneliya Angelova
Today at 10:08:44 AM
10:08 AM
за да мимикира точно както е автоматичното генериране на репорта - може ли да е за фиксирана седмица назад
Today at 10:08:57 AM
10:08
филтъра и това което се изписва във пдфа
Lukas Kovalik
Today at 10:09:09 AM
10:09 AM
и аз се чудих, то за това с получи разминаване
Today at 10:10:26 AM
10:10
PDF мисля че беше 7 дни на зад и activity бяха от понеделник до неделя
Today at 10:10:46 AM
10:10
реално ако се пуска от крон йоб трябва да е ок, само ръчно
Aneliya Angelova
Today at 10:10:53 AM
10:10 AM
да
Lukas Kovalik
Today at 10:11:02 AM
10:11 AM
направих го сега 7 дни на всякъде
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Aneliya Angelova
Today at 10:11:03 AM
10:11 AM
и на двете места да стане понеделник-неделя
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 10:11:46 AM
10:11 AM
но реално ако искаме ретроспективно да пуснем нещо седмично което не е минало в сряда ще е от среяда до четвъртък
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Aneliya Angelova
Today at 10:13:21 AM
10:13 AM
Галя казва, че по скоро иска винаги да се пускат от понеделник до неделя, за да може да има бекъп вариант ако трябва по някаква причина ръчно да се рерънне седмичен репорт на клиент
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 10:13:47 AM
10:13 AM
да, ами добре ще ги сменя, на старите репорти
(edited)
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Aneliya Angelova
Today at 10:14:00 AM
10:14 AM
оки
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 10:15:09 AM
10:15
тайтъла на репорта и Data Source секцията ще показват правилния период нали - понеже това се разминаваше до сега
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
сега забелязах че стария count който се ползва в при контрол на достъп цанАццессАиРепор
сега забелязах че стария count който се ползва в при контрол на достъп цанАццессАиРепор
Shift + Return to add a new line
Shift + Return
to add a new line
Conversation marked as unread from this message
Channel
Finder FileEditViewGoWindowHelp‹ > 0 • { Userpilot Introducti….. in 44 m• =@ meet.google.com/landing?authuser=lukas.kovalik@jiminny.comБГ100% & 8 Thu 23 Apr 10:46:38= [1 Google Meet10:46 AM • Thu, Apr 23+MeetingsD* CallsSecure video conferencingfor everyoneConnect, collaborate, and celebrate from anywhere withGoogle MeetEx New meetingEnter a code or nicknameJoin11:30 AMUserpilot Introduction3:00 PMSupport Daily5:30 PMLukas/Stefka 121From your Google Calendar account: [EMAIL] more about Google Meet...
|
73471
|
|
73472
|
NULL
|
0
|
2026-04-23T07:46:31.517548+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776930391517_m2.jpg...
|
Slack
|
Aneliya Angelova, Nikolay Yankov, Steliyan Georgie Aneliya Angelova, Nikolay Yankov, Steliyan Georgiev (DM) - Jiminny Inc - 1 new item - Slack...
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
c-learning-people
confusion-clinic
curiosity_lab
deal-insights-dev
engineering
frontend
general
infra-changes
jiminny-bg
people-with-copilot-licences
people-with-zoom-phone-licences
platform-team
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Stoyan Tomov
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Nikolay Yankov
Petko Kashinski
Aneliya Angelova
Nikolay Nikolov
Mario Georgiev
Todor Stamatov
Gabriela Dureva
Vasil Vasilev
Galya Dimitrova
Stefka Stoyanova
Stoyan Tanev
Nikolay Ivanov
Toast
Jira Cloud
Messages
Messages
Add canvas
Add canvas
Files
Files
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Aneliya Angelova
Today at 10:08:44 AM
10:08 AM
за да мимикира точно както е автоматичното генериране на репорта - може ли да е за фиксирана седмица назад
Today at 10:08:57 AM
10:08
филтъра и това което се изписва във пдфа
Lukas Kovalik
Today at 10:09:09 AM
10:09 AM
и аз се чудих, то за това с получи разминаване
Today at 10:10:26 AM
10:10
PDF мисля че беше 7 дни на зад и activity бяха от понеделник до неделя
Today at 10:10:46 AM
10:10
реално ако се пуска от крон йоб трябва да е ок, само ръчно
Aneliya Angelova
Today at 10:10:53 AM
10:10 AM
да
Lukas Kovalik
Today at 10:11:02 AM
10:11 AM
направих го сега 7 дни на всякъде
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Aneliya Angelova
Today at 10:11:03 AM
10:11 AM
и на двете места да стане понеделник-неделя
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 10:11:46 AM
10:11 AM
но реално ако искаме ретроспективно да пуснем нещо седмично което не е минало в сряда ще е от среяда до четвъртък
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Aneliya Angelova
Today at 10:13:21 AM
10:13 AM
Галя казва, че по скоро иска винаги да се пускат от понеделник до неделя, за да може да има бекъп вариант ако трябва по някаква причина ръчно да се рерънне седмичен репорт на клиент
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 10:13:47 AM
10:13 AM
да, ами добре ще ги сменя, на старите репорти
(edited)
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Aneliya Angelova
Today at 10:14:00 AM
10:14 AM
оки
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 10:15:09 AM
10:15
тайтъла на репорта и Data Source секцията ще показват правилния период нали - понеже това се разминаваше до сега
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
сега забелязах че стария count който се ползва в при контрол на
сега забелязах че стария count който се ползва в при контрол на
Shift + Return to add a new line
Shift + Return
to add a new line
Conversation marked as unread from this message
Channel...
|
[{"role":"AXPopUpButton","text [{"role":"AXPopUpButton","text":"Switch workspaces… (Jiminny Inc) Has new messages","depth":14,"bounds":{"left":0.0056515955,"top":0.058260176,"width":0.011968086,"height":0.028731046},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"Home","depth":14,"bounds":{"left":0.0029920214,"top":0.10055866,"width":0.017287234,"height":0.054269753},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Home","depth":16,"bounds":{"left":0.0066489363,"top":0.13806863,"width":0.009973404,"height":0.0103751},"role_description":"text"},{"role":"AXRadioButton","text":"DMs","depth":14,"bounds":{"left":0.0029920214,"top":0.15482841,"width":0.017287234,"height":0.054269753},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"DMs","depth":16,"bounds":{"left":0.0076462766,"top":0.19233839,"width":0.007978723,"height":0.0103751},"role_description":"text"},{"role":"AXRadioButton","text":"Activity","depth":14,"bounds":{"left":0.0029920214,"top":0.20909816,"width":0.017287234,"height":0.054269753},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Activity","depth":16,"bounds":{"left":0.004986702,"top":0.24660814,"width":0.012965426,"height":0.0103751},"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":14,"bounds":{"left":0.0029920214,"top":0.26336792,"width":0.017287234,"height":0.054269753},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":16,"bounds":{"left":0.0076462766,"top":0.3008779,"width":0.0076462766,"height":0.0103751},"role_description":"text"},{"role":"AXRadioButton","text":"Later","depth":14,"bounds":{"left":0.0029920214,"top":0.31763768,"width":0.017287234,"height":0.054269753},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Later","depth":16,"bounds":{"left":0.00731383,"top":0.35514766,"width":0.008643617,"height":0.0103751},"role_description":"text"},{"role":"AXRadioButton","text":"More…","depth":14,"bounds":{"left":0.0029920214,"top":0.3719074,"width":0.017287234,"height":0.054269753},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More","depth":16,"bounds":{"left":0.006981383,"top":0.4094174,"width":0.008976064,"height":0.0103751},"role_description":"text"},{"role":"AXStaticText","text":"Unreads","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Threads","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Huddles","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Drafts & sent","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Directories","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"jiminny-x-integration-app","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"platform-inner-team","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"ai-chapter","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"alerts","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"backend","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"c-learning-people","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"confusion-clinic","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"curiosity_lab","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"deal-insights-dev","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"engineering","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"frontend","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"general","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"infra-changes","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"jiminny-bg","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"people-with-copilot-licences","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"people-with-zoom-phone-licences","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"platform-team","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"platform-tickets","depth":23,"bounds":{"left":0.042220745,"top":0.09177973,"width":0.034906916,"height":0.0071827616},"role_description":"text"},{"role":"AXStaticText","text":"product_launches","depth":23,"bounds":{"left":0.042220745,"top":0.10694334,"width":0.03856383,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"random","depth":23,"bounds":{"left":0.042220745,"top":0.1292897,"width":0.01662234,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"releases","depth":23,"bounds":{"left":0.042220745,"top":0.15163608,"width":0.01761968,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"sofia-office","depth":23,"bounds":{"left":0.042220745,"top":0.17398244,"width":0.024268618,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"support","depth":23,"bounds":{"left":0.042220745,"top":0.1963288,"width":0.016954787,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"thank-yous","depth":23,"bounds":{"left":0.042220745,"top":0.21867518,"width":0.024268618,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"the_people_of_jiminny","depth":23,"bounds":{"left":0.042220745,"top":0.24102154,"width":0.04488032,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Stoyan Tomov","depth":23,"bounds":{"left":0.042220745,"top":0.29369512,"width":0.030585106,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":23,"bounds":{"left":0.042220745,"top":0.3160415,"width":0.03756649,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":",","depth":23,"bounds":{"left":0.07945479,"top":0.3160415,"width":0.0063164895,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Yankov","depth":23,"bounds":{"left":0.08211436,"top":0.3160415,"width":0.014295213,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":",","depth":23,"bounds":{"left":0.09607713,"top":0.33359936,"width":0.0003324468,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Steliyan Georgiev","depth":23,"bounds":{"left":0.09607713,"top":0.33359936,"width":0.0003324468,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Yankov","depth":23,"bounds":{"left":0.042220745,"top":0.33838788,"width":0.032912236,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Petko Kashinski","depth":23,"bounds":{"left":0.042220745,"top":0.36073422,"width":0.034242023,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":23,"bounds":{"left":0.042220745,"top":0.3830806,"width":0.03756649,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Nikolov","depth":23,"bounds":{"left":0.042220745,"top":0.40542698,"width":0.034242023,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Mario Georgiev","depth":23,"bounds":{"left":0.042220745,"top":0.42777336,"width":0.033909574,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Todor Stamatov","depth":23,"bounds":{"left":0.042220745,"top":0.4501197,"width":0.034242023,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Gabriela Dureva","depth":23,"bounds":{"left":0.042220745,"top":0.47246608,"width":0.03523936,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Vasil Vasilev","depth":23,"bounds":{"left":0.042220745,"top":0.49481246,"width":0.026263298,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Galya Dimitrova","depth":23,"bounds":{"left":0.042220745,"top":0.5171588,"width":0.034906916,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Stefka Stoyanova","depth":23,"bounds":{"left":0.042220745,"top":0.5395052,"width":0.03756649,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Stoyan Tanev","depth":23,"bounds":{"left":0.042220745,"top":0.56185156,"width":0.028922873,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Ivanov","depth":23,"bounds":{"left":0.042220745,"top":0.58419794,"width":0.031914894,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Toast","depth":23,"bounds":{"left":0.042220745,"top":0.6368715,"width":0.011968086,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Jira Cloud","depth":23,"bounds":{"left":0.042220745,"top":0.6592179,"width":0.021609042,"height":0.014365523},"role_description":"text"},{"role":"AXRadioButton","text":"Messages","depth":17,"bounds":{"left":0.10206117,"top":0.09177973,"width":0.030585106,"height":0.030327214},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Messages","depth":19,"bounds":{"left":0.111369684,"top":0.10055866,"width":0.01861702,"height":0.012769354},"role_description":"text"},{"role":"AXRadioButton","text":"Add canvas","depth":18,"bounds":{"left":0.13397606,"top":0.09177973,"width":0.033909574,"height":0.030327214},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Add canvas","depth":20,"bounds":{"left":0.14328457,"top":0.10055866,"width":0.021941489,"height":0.012769354},"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":17,"bounds":{"left":0.16921543,"top":0.09177973,"width":0.020944148,"height":0.030327214},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":19,"bounds":{"left":0.17852394,"top":0.10055866,"width":0.008976064,"height":0.012769354},"role_description":"text"},{"role":"AXPopUpButton","text":"Add and Edit Channel Tabs","depth":17,"bounds":{"left":0.19115691,"top":0.09177973,"width":0.010970744,"height":0.030327214},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Canvas","depth":17,"bounds":{"left":0.096409574,"top":0.0518755,"width":0.015625,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"List","depth":17,"bounds":{"left":0.096409574,"top":0.0518755,"width":0.0076462766,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Folder","depth":17,"bounds":{"left":0.096409574,"top":0.0518755,"width":0.013962766,"height":0.0007980846},"role_description":"text"},{"role":"AXPopUpButton","text":"Jump to date","depth":23,"bounds":{"left":0.15026596,"top":0.12689546,"width":0.025265958,"height":0.022346368},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Aneliya Angelova","depth":24,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"role_description":"text"},{"role":"AXLink","text":"Today at 10:08:44 AM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:08 AM","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"за да мимикира точно както е автоматичното генериране на репорта - може ли да е за фиксирана седмица назад","depth":25,"role_description":"text"},{"role":"AXLink","text":"Today at 10:08:57 AM","depth":25,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:08","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"филтъра и това което се изписва във пдфа","depth":25,"role_description":"text"},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"role_description":"text"},{"role":"AXLink","text":"Today at 10:09:09 AM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:09 AM","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"и аз се чудих, то за това с получи разминаване","depth":25,"role_description":"text"},{"role":"AXLink","text":"Today at 10:10:26 AM","depth":25,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:10","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"PDF мисля че беше 7 дни на зад и activity бяха от понеделник до неделя","depth":25,"role_description":"text"},{"role":"AXLink","text":"Today at 10:10:46 AM","depth":25,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:10","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"реално ако се пуска от крон йоб трябва да е ок, само ръчно","depth":25,"role_description":"text"},{"role":"AXButton","text":"Aneliya Angelova","depth":24,"bounds":{"left":0.11801862,"top":0.11572227,"width":0.038896278,"height":0.011173184},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.16323139,"top":0.11572227,"width":0.0029920214,"height":0.009577015},"role_description":"text"},{"role":"AXLink","text":"Today at 10:10:53 AM","depth":24,"bounds":{"left":0.16589096,"top":0.11572227,"width":0.01761968,"height":0.008778931},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:10 AM","depth":25,"bounds":{"left":0.16589096,"top":0.11572227,"width":0.01761968,"height":0.008778931},"role_description":"text"},{"role":"AXStaticText","text":"да","depth":25,"bounds":{"left":0.11801862,"top":0.12849163,"width":0.0056515955,"height":0.014365523},"role_description":"text"},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"bounds":{"left":0.11801862,"top":0.15083799,"width":0.030917553,"height":0.017557861},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.14860372,"top":0.15243416,"width":0.0029920214,"height":0.014365523},"role_description":"text"},{"role":"AXLink","text":"Today at 10:11:02 AM","depth":24,"bounds":{"left":0.1512633,"top":0.15482841,"width":0.01761968,"height":0.011173184},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:11 AM","depth":25,"bounds":{"left":0.1512633,"top":0.15482841,"width":0.01761968,"height":0.011173184},"role_description":"text"},{"role":"AXStaticText","text":"направих го сега 7 дни на всякъде","depth":25,"bounds":{"left":0.11801862,"top":0.16999201,"width":0.078457445,"height":0.014365523},"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"bounds":{"left":0.13730054,"top":0.13727055,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"bounds":{"left":0.14793883,"top":0.13727055,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"bounds":{"left":0.15857713,"top":0.13727055,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"bounds":{"left":0.16921543,"top":0.13727055,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"bounds":{"left":0.17985372,"top":0.13727055,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"bounds":{"left":0.22340426,"top":0.13727055,"width":0.0003324468,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"bounds":{"left":0.22340426,"top":0.13727055,"width":0.0003324468,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"bounds":{"left":0.22340426,"top":0.13727055,"width":0.0003324468,"height":0.025538707},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Aneliya Angelova","depth":24,"bounds":{"left":0.11801862,"top":0.19233839,"width":0.038896278,"height":0.017557861},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.16323139,"top":0.19393456,"width":0.0029920214,"height":0.014365523},"role_description":"text"},{"role":"AXLink","text":"Today at 10:11:03 AM","depth":24,"bounds":{"left":0.16589096,"top":0.1963288,"width":0.01761968,"height":0.011173184},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:11 AM","depth":25,"bounds":{"left":0.16589096,"top":0.1963288,"width":0.01761968,"height":0.011173184},"role_description":"text"},{"role":"AXStaticText","text":"и на двете места да стане понеделник-неделя","depth":25,"bounds":{"left":0.11801862,"top":0.21149242,"width":0.09042553,"height":0.031923383},"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"bounds":{"left":0.13730054,"top":0.17877094,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"bounds":{"left":0.14793883,"top":0.17877094,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"bounds":{"left":0.15857713,"top":0.17877094,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"bounds":{"left":0.16921543,"top":0.17877094,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"bounds":{"left":0.17985372,"top":0.17877094,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"bounds":{"left":0.22340426,"top":0.17877094,"width":0.0003324468,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"bounds":{"left":0.22340426,"top":0.17877094,"width":0.0003324468,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"bounds":{"left":0.22340426,"top":0.17877094,"width":0.0003324468,"height":0.025538707},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"bounds":{"left":0.11801862,"top":0.25139666,"width":0.030917553,"height":0.017557861},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.14860372,"top":0.2529928,"width":0.0029920214,"height":0.014365523},"role_description":"text"},{"role":"AXLink","text":"Today at 10:11:46 AM","depth":24,"bounds":{"left":0.1512633,"top":0.25538707,"width":0.01761968,"height":0.011173184},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:11 AM","depth":25,"bounds":{"left":0.1512633,"top":0.25538707,"width":0.01761968,"height":0.011173184},"role_description":"text"},{"role":"AXStaticText","text":"но реално ако искаме ретроспективно да пуснем нещо седмично което не е минало в сряда ще е от среяда до четвъртък","depth":25,"bounds":{"left":0.11801862,"top":0.27055067,"width":0.10172872,"height":0.049481247},"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"bounds":{"left":0.13730054,"top":0.23782921,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"bounds":{"left":0.14793883,"top":0.23782921,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"bounds":{"left":0.15857713,"top":0.23782921,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"bounds":{"left":0.16921543,"top":0.23782921,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"bounds":{"left":0.17985372,"top":0.23782921,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"bounds":{"left":0.22340426,"top":0.23782921,"width":0.0003324468,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"bounds":{"left":0.22340426,"top":0.23782921,"width":0.0003324468,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"bounds":{"left":0.22340426,"top":0.23782921,"width":0.0003324468,"height":0.025538707},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Aneliya Angelova","depth":24,"bounds":{"left":0.11801862,"top":0.32801276,"width":0.038896278,"height":0.017557861},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.16323139,"top":0.32960895,"width":0.0029920214,"height":0.014365523},"role_description":"text"},{"role":"AXLink","text":"Today at 10:13:21 AM","depth":24,"bounds":{"left":0.16589096,"top":0.3320032,"width":0.01761968,"height":0.011173184},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:13 AM","depth":25,"bounds":{"left":0.16589096,"top":0.3320032,"width":0.01761968,"height":0.011173184},"role_description":"text"},{"role":"AXStaticText","text":"Галя казва, че по скоро иска винаги да се пускат от понеделник до неделя, за да може да има бекъп вариант ако трябва по някаква причина ръчно да се рерънне седмичен репорт на клиент","depth":25,"bounds":{"left":0.11801862,"top":0.3471668,"width":0.10372341,"height":0.08459697},"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"bounds":{"left":0.13730054,"top":0.31444532,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"bounds":{"left":0.14793883,"top":0.31444532,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"bounds":{"left":0.15857713,"top":0.31444532,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"bounds":{"left":0.16921543,"top":0.31444532,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"bounds":{"left":0.17985372,"top":0.31444532,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"bounds":{"left":0.22340426,"top":0.31444532,"width":0.0003324468,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"bounds":{"left":0.22340426,"top":0.31444532,"width":0.0003324468,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"bounds":{"left":0.22340426,"top":0.31444532,"width":0.0003324468,"height":0.025538707},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"bounds":{"left":0.11801862,"top":0.43974462,"width":0.030917553,"height":0.017557861},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.14860372,"top":0.44134077,"width":0.0029920214,"height":0.014365523},"role_description":"text"},{"role":"AXLink","text":"Today at 10:13:47 AM","depth":24,"bounds":{"left":0.1512633,"top":0.44373503,"width":0.01761968,"height":0.011173184},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:13 AM","depth":25,"bounds":{"left":0.1512633,"top":0.44373503,"width":0.01761968,"height":0.011173184},"role_description":"text"},{"role":"AXStaticText","text":"да, ами добре ще ги сменя, на старите репорти","depth":25,"bounds":{"left":0.11801862,"top":0.45889863,"width":0.0887633,"height":0.031923383},"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"bounds":{"left":0.13696809,"top":0.47805268,"width":0.0013297872,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"(edited)","depth":25,"bounds":{"left":0.13829787,"top":0.47805268,"width":0.014295213,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"bounds":{"left":0.15226063,"top":0.47805268,"width":0.0013297872,"height":0.012769354},"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"bounds":{"left":0.13730054,"top":0.42617717,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"bounds":{"left":0.14793883,"top":0.42617717,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"bounds":{"left":0.15857713,"top":0.42617717,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"bounds":{"left":0.16921543,"top":0.42617717,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"bounds":{"left":0.17985372,"top":0.42617717,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"bounds":{"left":0.22340426,"top":0.42617717,"width":0.0003324468,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"bounds":{"left":0.22340426,"top":0.42617717,"width":0.0003324468,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"bounds":{"left":0.22340426,"top":0.42617717,"width":0.0003324468,"height":0.025538707},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Aneliya Angelova","depth":24,"bounds":{"left":0.11801862,"top":0.49880287,"width":0.038896278,"height":0.017557861},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.16323139,"top":0.50039905,"width":0.0029920214,"height":0.014365523},"role_description":"text"},{"role":"AXLink","text":"Today at 10:14:00 AM","depth":24,"bounds":{"left":0.16589096,"top":0.5027933,"width":0.01761968,"height":0.011173184},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:14 AM","depth":25,"bounds":{"left":0.16589096,"top":0.5027933,"width":0.01761968,"height":0.011173184},"role_description":"text"},{"role":"AXStaticText","text":"оки","depth":25,"bounds":{"left":0.11801862,"top":0.5179569,"width":0.00831117,"height":0.014365523},"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"bounds":{"left":0.13730054,"top":0.48523542,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"bounds":{"left":0.14793883,"top":0.48523542,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"bounds":{"left":0.15857713,"top":0.48523542,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"bounds":{"left":0.16921543,"top":0.48523542,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"bounds":{"left":0.17985372,"top":0.48523542,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"bounds":{"left":0.22340426,"top":0.48523542,"width":0.0003324468,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"bounds":{"left":0.22340426,"top":0.48523542,"width":0.0003324468,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"bounds":{"left":0.22340426,"top":0.48523542,"width":0.0003324468,"height":0.025538707},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 10:15:09 AM","depth":25,"bounds":{"left":0.105053194,"top":0.5442937,"width":0.010305851,"height":0.011173184},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:15","depth":26,"bounds":{"left":0.105053194,"top":0.5442937,"width":0.010305851,"height":0.011173184},"role_description":"text"},{"role":"AXStaticText","text":"тайтъла на репорта и Data Source секцията ще показват правилния период нали - понеже това се разминаваше до сега","depth":25,"bounds":{"left":0.11801862,"top":0.54189944,"width":0.09940159,"height":0.049481247},"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"bounds":{"left":0.13730054,"top":0.5171588,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"bounds":{"left":0.14793883,"top":0.5171588,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"bounds":{"left":0.15857713,"top":0.5171588,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"bounds":{"left":0.16921543,"top":0.5171588,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"bounds":{"left":0.17985372,"top":0.5171588,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"bounds":{"left":0.22340426,"top":0.5171588,"width":0.0003324468,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"bounds":{"left":0.22340426,"top":0.5171588,"width":0.0003324468,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"bounds":{"left":0.22340426,"top":0.5171588,"width":0.0003324468,"height":0.025538707},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"сега забелязах че стария count който се ползва в при контрол на","depth":23,"bounds":{"left":0.10372341,"top":0.6097366,"width":0.118351065,"height":0.047885075},"value":"сега забелязах че стария count който се ползва в при контрол на","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"сега забелязах че стария count който се ползва в при контрол на","depth":25,"bounds":{"left":0.10771277,"top":0.6177175,"width":0.109707445,"height":0.031923383},"role_description":"text"},{"role":"AXButton","text":"Shift + Return to add a new line","depth":20,"bounds":{"left":0.17121011,"top":0.6935355,"width":0.048537236,"height":0.012769354},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Shift + Return","depth":21,"bounds":{"left":0.17121011,"top":0.6943336,"width":0.021609042,"height":0.0103751},"role_description":"text"},{"role":"AXStaticText","text":"to add a new line","depth":21,"bounds":{"left":0.1924867,"top":0.6943336,"width":0.027260639,"height":0.0103751},"role_description":"text"},{"role":"AXStaticText","text":"Conversation marked as unread from this message","depth":11,"bounds":{"left":0.0,"top":0.7126895,"width":0.02825798,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Channel","depth":11,"bounds":{"left":0.0,"top":0.7126895,"width":0.017287234,"height":0.0007980846},"role_description":"text"}]...
|
5050377422629350058
|
-1280970121151231934
|
idle
|
hybrid
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
c-learning-people
confusion-clinic
curiosity_lab
deal-insights-dev
engineering
frontend
general
infra-changes
jiminny-bg
people-with-copilot-licences
people-with-zoom-phone-licences
platform-team
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Stoyan Tomov
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Nikolay Yankov
Petko Kashinski
Aneliya Angelova
Nikolay Nikolov
Mario Georgiev
Todor Stamatov
Gabriela Dureva
Vasil Vasilev
Galya Dimitrova
Stefka Stoyanova
Stoyan Tanev
Nikolay Ivanov
Toast
Jira Cloud
Messages
Messages
Add canvas
Add canvas
Files
Files
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Aneliya Angelova
Today at 10:08:44 AM
10:08 AM
за да мимикира точно както е автоматичното генериране на репорта - може ли да е за фиксирана седмица назад
Today at 10:08:57 AM
10:08
филтъра и това което се изписва във пдфа
Lukas Kovalik
Today at 10:09:09 AM
10:09 AM
и аз се чудих, то за това с получи разминаване
Today at 10:10:26 AM
10:10
PDF мисля че беше 7 дни на зад и activity бяха от понеделник до неделя
Today at 10:10:46 AM
10:10
реално ако се пуска от крон йоб трябва да е ок, само ръчно
Aneliya Angelova
Today at 10:10:53 AM
10:10 AM
да
Lukas Kovalik
Today at 10:11:02 AM
10:11 AM
направих го сега 7 дни на всякъде
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Aneliya Angelova
Today at 10:11:03 AM
10:11 AM
и на двете места да стане понеделник-неделя
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 10:11:46 AM
10:11 AM
но реално ако искаме ретроспективно да пуснем нещо седмично което не е минало в сряда ще е от среяда до четвъртък
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Aneliya Angelova
Today at 10:13:21 AM
10:13 AM
Галя казва, че по скоро иска винаги да се пускат от понеделник до неделя, за да може да има бекъп вариант ако трябва по някаква причина ръчно да се рерънне седмичен репорт на клиент
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 10:13:47 AM
10:13 AM
да, ами добре ще ги сменя, на старите репорти
(edited)
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Aneliya Angelova
Today at 10:14:00 AM
10:14 AM
оки
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 10:15:09 AM
10:15
тайтъла на репорта и Data Source секцията ще показват правилния период нали - понеже това се разминаваше до сега
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
сега забелязах че стария count който се ползва в при контрол на
сега забелязах че стария count който се ползва в при контрол на
Shift + Return to add a new line
Shift + Return
to add a new line
Conversation marked as unread from this message
Channel
HomeActivityLateMoreSlackJiminny …..v* Uiduiorii-uckeis# product_launches# randoma releases# soha-office# support# thank-yous# the people of jimi..0 Direct messagesStoyan Tomov3 Aneliya Angelova, ...Nikolay Yankovf Petko Kashinski. Aneliya Angelova Ce. Nikolay NikolovMario GeorgievSe: Todor StamatovA Gabriela DurevalVasil Vasilev. Galva DimitrovaStefka Stoyanova v*% Stoyan TanevNikolav Ivanov:AppsToast" Jira CloudmistonWindowhelp* Aneliya Angelova, ...MessagesAdd canvaUr FilesLukas Kovalik 10:11 AMнаправих го сега 7 дни на всякъдеAneliva Angelova 10:11 AMIи на лвете места ла стане понелелник.HeneляLukas Kovalik 10,:11 AMно реално ако искаме ретроспективно дапуснем нещо седмично което не е минало всряда ще е от среяда до четвьртькAneliva Angelova • 10:13 AMГаля казва, че по скоро иска винаги ла сепускат от понелелник ло нелеля. за ла можеда има бекъп вариант ако трябва по някаквапоичина оьчно ла се перьнне селмичен .Lukas Kovallk 10:13 AMла, ами лобое ше ги сменя. на ставитерепорти (ediAneliva Angelova 10:14 AMтайтьла на депорта и Data Source секцияташе показват поавилния периол нали -понеже това се пазминаваше ло сегасега забелязах че стария сount които се ползвав при контрол н+ Aa €FV faVsco.js°9 JY-20157-AJ-report-not-send-notificaticProiect= custom.log= laravel.l0g© EventServiceProvider.phpAskJminnykeporacuvilyservice.org© HubspotJournalServiceProvider.p© HubspotWebhookServiceProvider© JiminnyServiceProvider.php(c) PlanhatServiceProvider.ong) Automatedreporiskepository.ono xpnp api vz.ohp© ProphetHandlerServiceProvider.p© PusherServiceProvider.php29.07.25 NIkolov© QueueLogServiceProvider.php3.09.25 vanov© QueueStatsdServiceProvider.php 23.10.25 Ivanoy© ResponseMacroServiceProvider.c 23.10.25 Ivanov267© RouteServiceProvider.php3.09.25 Ivanov© SsoServiceProvider.php3.09.25 Ivanov© UtilServiceProvider.php3.09.25 Ivanov© AutomatedReportsCommand.phpA SF [jiminny@localhost]-repori-not-generated.dlade.png©) PlannatActivityListener.php# console PRODc) ViewercuaroserviceProvider.ono> D Queue3.09.25 Ivanovv → Repositories3.09.2o Ivanov> CAi3.09.25 Ivanov> → AutoScorina3.09.25>@ Calendar3.09.25 Ivanovverm3.09.5© AccountRepository.php3.09.25© ContactRepository.php3.09.25© ContactRoleRepository.php3.09.25 vanov© CrmConfigurationRepository.p3.09.25© CrmEntityRepository.php3.09.25© FieldDataRepository.php© FieldRepository.php© LayoutEntityRepository.php© LayoutRepository.php© LeadRepository.php© OpportunityRepository.php© ProfileRepository.php© RecordTypeFieldValuesReposi© StageRepository.php© SyncBatchRepository.php> C Geography© ActiveStreamsRepository.php© ActivityCommentRepository.php© ActivityLoaRepository.pho© ActivitvMessageRepository.php19.09.25 Nikolov@ActivitvMomentRepositorv.oh71.0426 [EMAIL]@ActivitvRepositorv.ohn21.04.26 Kovalik@ActivitvSearchFilterRepositorv.oh21.04.26 Kovalik@ ActivitvShareRepositorv.ohn21.04.26 Kovalik@ ActivitvUoloadSettinaRenositorv.| 21.04.26 Kovalik© AiPromptRepository.php@AckAnvthinaRenository.nhr21OA26 KovAlil9 AutomatedReportsRepository.php 21.04.26 Kovalik@ CallimportRepository.php210426 Kovalil(C) CoachinaFeedhackRenasitory nhil 21.04 26 Kovalil2841286© DatabaseActivities.phpclass AutomatedReportsrepos1torypublic function getPaginatedUserReportslC) DealsRepository.php X& console (EUreturn Squery->paginate(SresultsPerPage, ['*'],'pagename. page, spages,=142class I wmA36^public function ge$qb = Activity::with(l->where column.bublic function countUserReports(User $user): int->Wherevarel corecurn Aucomacedkeportkesult:.query->WhereNotNULL co,'generated at')->WherenotNuLue coll>whereras relsentat')function (Sa) use (Suser): 'team_id'. Suser->qetTeamIdO)->orderyscriter*// Should we filter ac->whereJsoncontains column:'reciplents->users'. Suser->getidon:152return $qb->get()->all->counto:154155 @› ic function getStagesusagesublic function countAllUserRenorts(User Suser): int.180 C › ic function getConfigureturnAutomatedRenortResult.queruol›ic function getPipelir->whereNotNul1d columns'aenerated at')lIcent ati)function ($a) use (Suser) {Reriectate function cnpateniSo->w229->where.soncontains/recinients->usens' Susen->aettdone->whereHas(re231"'report'. fn (Builder $q) => Sthis->appLyUserAccessScope($q, $ 232233pplies all applicablereturn stringil** Restrict a query on the automated_reports table to reports the given user is allowed 235, gee funation visitcoueusadesrivate function applyUserAccessScope(Builder Squery, User $user): voidate function hvdrateStSarounid = Susen->ae+GrounTdo•• 269param int[] $dealIdsCauoni->where( column: 'automated_reports.team_id', Suser->getTeamId)->where(function (Builder $a) use (SuserId, SgroupId): void ‹Ca-swhone. leonfontaine/ column''automated reports.recipients->users', SuserId)201ate function fetchStad"userollot Introouct... In 44mБГ100% S2Thu 23 Apr 10:46:31CascadeAutomated Reports LO+O •can you reuse the method @AutomatedReportsRepository-php#L302-319Tnougnttor 4sRead AutomatedReportsRepositorv.phpThought for 1s• AutomatedReportsRepository.php+1 -4Done. countAllUserReports now reuses applyUserAccessScope, keeping access control logic in one place.Note: appLyUserAccessScope uses automated reports. -prefixed columns. Inside wherehas('report", ..., the subquery runson the automated reports table, so the prefix resolves correctly.O dl ***1 file +1-4 >Ask anvthina (₴4L)+ « CodeClaude Qnus 4.7 Medium* Reiect allAccent alliWN Windsurf Toams 252.57Po. 4 space...
|
NULL
|
|
73442
|
NULL
|
0
|
2026-04-23T07:41:06.893818+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776930066893_m2.jpg...
|
Finder
|
Work
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Favourites
jiminny
AirDrop
Recents
Applications
Do Favourites
jiminny
AirDrop
Recents
Applications
Documents
Downloads
lukas
iCloud
iCloud Drive
Sync folder
Locations
DXP4800PLUS-B5F
Eject
Network
Tags
CRM
Orange
Red
Yellow
Green
Blue
Purple
All Tags…
Name
Date Modified
Size
Kind
2026
Today at 10:32
--
Folder
CleanShot 2026-04-23 at 09.45.36.mp4
Today at 10:32
1,74 GB
MPEG-4 movie
Daily 2026-04-22.mp4
Yesterday at 10:21
1,36 GB
MPEG-4 movie
Refinement 2026-04-06.mp4
21 Apr 2026 at 11:02
2,41 GB
MPEG-4 movie
Daily 2026-04-21.mp4
21 Apr 2026 at 10:00
567,8 MB
MPEG-4 movie
Refinement 2026-04-20.mp4
20 Apr 2026 at 16:56
4,25 GB
MPEG-4 movie
Daily 2026-04-20.mp4
20 Apr 2026 at 10:06
698,5 MB
MPEG-4 movie
Daily 2026-04-17.mp4
17 Apr 2026 at 10:16
1,16 GB
MPEG-4 movie
Daily 2026-04-16.mp4
16 Apr 2026 at 10:00
513,4 MB
MPEG-4 movie
Planning 2026-04-15.mp4
15 Apr 2026 at 11:14
2,75 GB
MPEG-4 movie
Retro 2026-04-14.mp4
14 Apr 2026 at 17:37
1,44 GB
MPEG-4 movie
Daily 2026-04-14.mp4
14 Apr 2026 at 10:09
924,4 MB
MPEG-4 movie
User pilot (Adi) 2026-04-09.mp4
9 Apr 2026 at 14:47
362,6 MB
MPEG-4 movie
Daily 2026-04-09.mp4
9 Apr 2026 at 10:07
748,8 MB
MPEG-4 movie
Daily 2026-04-08.mp4
8 Apr 2026 at 10:13
1,04 GB
MPEG-4 movie
Daily 2026-04-07.mp4
7 Apr 2026 at 10:01
575,5 MB
MPEG-4 movie
Daily 2026-04-06.mp4
6 Apr 2026 at 10:08
720,5 MB
MPEG-4 movie
Daily 2026-04-03.mp4
3 Apr 2026 at 10:21
1,02 GB
MPEG-4 movie
Planning 2026-04-01 & task split.mp4
1 Apr 2026 at 12:20
4,68 GB
MPEG-4 movie
Retro 2026-03-31.mp4
31 Mar 2026 at 18:29
3,4 GB
MPEG-4 movie
Daily 2026-03-31.mp4
31 Mar 2026 at 10:10
923,6 MB
MPEG-4 movie
Refinement 2026-03-30.mp4
30 Mar 2026 at 17:12
2,77 GB
MPEG-4 movie
Daily 2026-03-30.mp4
30 Mar 2026 at 10:05
641,8 MB
MPEG-4 movie
Daily 2026-03-27.mp4
27 Mar 2026 at 10:09
884,3 MB
MPEG-4 movie
Daily 2026-03-26.mp4
26 Mar 2026 at 9:59
476,6 MB
MPEG-4 movie
Daily 2026-03-24.mp4
24 Mar 2026 at 10:00
550,8 MB
MPEG-4 movie
Refinement 2026-03-23.mp4
23 Mar 2026 at 17:03
3,44 GB
MPEG-4 movie
Daily 2026-03-23.mp4
23 Mar 2026 at 10:00
438,9 MB
MPEG-4 movie
BE chapter 2026-03-20.mp4
20 Mar 2026 at 11:46
1,68 GB
MPEG-4 movie
Daily 2026-03-20.mp4
20 Mar 2026 at 10:06
430,4 MB
MPEG-4 movie
Planing 2026-03-18-converted.mp4
19 Mar 2026 at 12:01
2,38 GB
MPEG-4 movie
Refinement 2026-02-09-converted.mp4
19 Mar 2026 at 11:35
2,26 GB
MPEG-4 movie
Daily 2026-03-19.mp4
19 Mar 2026 at 9:57
386,3 MB
MPEG-4 movie
Review 2026-03-18.mp4
18 Mar 2026 at 16:20
705,8 MB
MPEG-4 movie
Planing 2026-03-18.mp4
18 Mar 2026 at 11:14
2,78 GB
MPEG-4 movie
Retro 2026-03-17.mp4
17 Mar 2026 at 17:40
1,53 GB
MPEG-4 movie
Daily 2026-03-17.mp4
17 Mar 2026 at 10:18
1,2 GB
MPEG-4 movie
Refinement 2026-03-16.mp4
16 Mar 2026 at 16:55
4,19 GB
MPEG-4 movie
Daily 2026-03-16.mp4
16 Mar 2026 at 10:02
592,2 MB
MPEG-4 movie
Daily 2026-03-13.mp4
13 Mar 2026 at 10:12
1,02 GB
MPEG-4 movie
1-1 2026-03-12.mp4
12 Mar 2026 at 18:35
637,6 MB
MPEG-4 movie
Daily 2026-03-12.mp4
12 Mar 2026 at 10:10
978,7 MB
MPEG-4 movie
Daily 2026-03-11.mp4
11 Mar 2026 at 10:06
798,7 MB
MPEG-4 movie
Daily 2026-03-10.mp4
10 Mar 2026 at 9:57
404,6 MB
MPEG-4 movie
Refinement 2026-03-09.mp4
9 Mar 2026 at 17:04
4,16 GB
MPEG-4 movie
Daily 2026-03-09.mp4
9 Mar 2026 at 9:56
319,7 MB
MPEG-4 movie
Daily 2026-03-06.mp4
6 Mar 2026 at 9:57
291,7 MB
MPEG-4 movie
Planning 2026-03-04.mp4
4 Mar 2026 at 11:09
2,62 GB
MPEG-4 movie
Daily 2026-03-02.mp4
2 Mar 2026 at 10:07
768,5 MB
MPEG-4 movie
Daily 2026-02-27.mp4
27 Feb 2026 at 10:02
546,8 MB
MPEG-4 movie
Daily 2026-02-26.mov
26 Feb 2026 at 9:53
96,6 MB
QT movie
Daily 2026-02-25.mov
25 Feb 2026 at 9:59
503,5 MB
QT movie
Opportunity-Contacts 2026-02-24.mp4
24 Feb 2026 at 12:03
791,7 MB
MPEG-4 movie
Daily 2026-02-24.mp4
24 Feb 2026 at 10:02
520,7 MB
MPEG-4 movie
Refinement 2026-02-23.mov
23 Feb 2026 at 16:31
2 GB
QT movie
Daily 2026-02-20 & Ani.mp4
20 Feb 2026 at 10:53
2,52 GB
MPEG-4 movie
Daily 2026-02-19.mp4
19 Feb 2026 at 9:54
234,2 MB
MPEG-4 movie
Review 2026-02-18.mp4
18 Feb 2026 at 16:25
925,1 MB
MPEG-4 movie
Planning 2026-02-18.mp4
18 Feb 2026 at 10:57
2,04 GB
MPEG-4 movie
Retro 2026-02-17.mp4
17 Feb 2026 at 17:38
1,31 GB
MPEG-4 movie
Refinement & P1 debugging 2026-02-16.mp4
16 Feb 2026 at 17:36
4,53 GB
MPEG-4 movie
SyncObjects3 2026-02-16.mp4...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Favourites","depth":6,"bounds":{"left":0.5046542,"top":0.061452515,"width":0.06216755,"height":0.015163607},"automation_id":"xSidebarHeader","role_description":"text"},{"role":"AXStaticText","text":"jiminny","depth":6,"bounds":{"left":0.51263297,"top":0.08140463,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"AirDrop","depth":6,"bounds":{"left":0.51263297,"top":0.103751,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"Recents","depth":6,"bounds":{"left":0.51263297,"top":0.12609737,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"Applications","depth":6,"bounds":{"left":0.51263297,"top":0.14844373,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"Documents","depth":6,"bounds":{"left":0.51263297,"top":0.1707901,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"Downloads","depth":6,"bounds":{"left":0.51263297,"top":0.19313647,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"lukas","depth":6,"bounds":{"left":0.51263297,"top":0.21548285,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"iCloud","depth":6,"bounds":{"left":0.5046542,"top":0.2434158,"width":0.06216755,"height":0.015163607},"automation_id":"xSidebarHeader","role_description":"text"},{"role":"AXStaticText","text":"iCloud Drive","depth":6,"bounds":{"left":0.51263297,"top":0.26336792,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"Sync folder","depth":6,"bounds":{"left":0.51263297,"top":0.2857143,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"Locations","depth":6,"bounds":{"left":0.5046542,"top":0.31364724,"width":0.06216755,"height":0.015163607},"automation_id":"xSidebarHeader","role_description":"text"},{"role":"AXStaticText","text":"DXP4800PLUS-B5F","depth":6,"bounds":{"left":0.51263297,"top":0.33359936,"width":0.043218084,"height":0.012769354},"role_description":"text"},{"role":"AXButton","text":"Eject","depth":6,"bounds":{"left":0.55651593,"top":0.33519554,"width":0.0043218085,"height":0.009577015},"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"Network","depth":6,"bounds":{"left":0.51263297,"top":0.35594574,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"Tags","depth":6,"bounds":{"left":0.5046542,"top":0.38387868,"width":0.06216755,"height":0.015163607},"automation_id":"xSidebarHeader","role_description":"text"},{"role":"AXStaticText","text":"CRM","depth":6,"bounds":{"left":0.51263297,"top":0.4038308,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"Orange","depth":6,"bounds":{"left":0.51263297,"top":0.42617717,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"Red","depth":6,"bounds":{"left":0.51263297,"top":0.44852355,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"Yellow","depth":6,"bounds":{"left":0.51263297,"top":0.4708699,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"Green","depth":6,"bounds":{"left":0.51263297,"top":0.49321628,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"Blue","depth":6,"bounds":{"left":0.51263297,"top":0.51556265,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"Purple","depth":6,"bounds":{"left":0.51263297,"top":0.53790903,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"All Tags…","depth":6,"bounds":{"left":0.51263297,"top":0.5602554,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"Name","depth":7,"bounds":{"left":0.5827792,"top":0.06624102,"width":0.011635638,"height":0.011173184},"role_description":"text"},{"role":"AXStaticText","text":"Date Modified","depth":7,"bounds":{"left":0.8656915,"top":0.06624102,"width":0.026928192,"height":0.011173184},"role_description":"text"},{"role":"AXStaticText","text":"Size","depth":7,"bounds":{"left":0.92586434,"top":0.06624102,"width":0.008976064,"height":0.011173184},"role_description":"text"},{"role":"AXStaticText","text":"Kind","depth":7,"bounds":{"left":0.9581117,"top":0.06624102,"width":0.00930851,"height":0.011173184},"role_description":"text"},{"role":"AXTextField","text":"2026","depth":7,"bounds":{"left":0.5827792,"top":0.08938547,"width":0.013297873,"height":0.012769354},"value":"2026","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Today at 10:32","depth":7,"bounds":{"left":0.8656915,"top":0.08938547,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"--","depth":7,"bounds":{"left":0.9494681,"top":0.08938547,"width":0.0056515955,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"Folder","depth":7,"bounds":{"left":0.9581117,"top":0.08938547,"width":0.014295213,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"CleanShot 2026-04-23 at 09.45.36.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.105347164,"width":0.084773935,"height":0.012769354},"value":"CleanShot 2026-04-23 at 09.45.36.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Today at 10:32","depth":7,"bounds":{"left":0.8656915,"top":0.105347164,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"1,74 GB","depth":7,"bounds":{"left":0.9368351,"top":0.105347164,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.105347164,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-22.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.121308856,"width":0.049534574,"height":0.012769354},"value":"Daily 2026-04-22.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Yesterday at 10:21","depth":7,"bounds":{"left":0.8656915,"top":0.121308856,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"1,36 GB","depth":7,"bounds":{"left":0.9368351,"top":0.121308856,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.121308856,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-04-06.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.13727055,"width":0.0625,"height":0.012769354},"value":"Refinement 2026-04-06.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"21 Apr 2026 at 11:02","depth":7,"bounds":{"left":0.8656915,"top":0.13727055,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"2,41 GB","depth":7,"bounds":{"left":0.9368351,"top":0.13727055,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.13727055,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-21.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.15323225,"width":0.04886968,"height":0.012769354},"value":"Daily 2026-04-21.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"21 Apr 2026 at 10:00","depth":7,"bounds":{"left":0.8656915,"top":0.15323225,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"567,8 MB","depth":7,"bounds":{"left":0.93351066,"top":0.15323225,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.15323225,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-04-20.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.16919394,"width":0.0625,"height":0.012769354},"value":"Refinement 2026-04-20.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"20 Apr 2026 at 16:56","depth":7,"bounds":{"left":0.8656915,"top":0.16919394,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"4,25 GB","depth":7,"bounds":{"left":0.9368351,"top":0.16919394,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.16919394,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-20.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.18515563,"width":0.049534574,"height":0.012769354},"value":"Daily 2026-04-20.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"20 Apr 2026 at 10:06","depth":7,"bounds":{"left":0.8656915,"top":0.18515563,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"698,5 MB","depth":7,"bounds":{"left":0.93351066,"top":0.18515563,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.18515563,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-17.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.20111732,"width":0.048204787,"height":0.012769354},"value":"Daily 2026-04-17.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"17 Apr 2026 at 10:16","depth":7,"bounds":{"left":0.8656915,"top":0.20111732,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"1,16 GB","depth":7,"bounds":{"left":0.9368351,"top":0.20111732,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.20111732,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-16.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.21707901,"width":0.04886968,"height":0.012769354},"value":"Daily 2026-04-16.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"16 Apr 2026 at 10:00","depth":7,"bounds":{"left":0.8656915,"top":0.21707901,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"513,4 MB","depth":7,"bounds":{"left":0.93351066,"top":0.21707901,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.21707901,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Planning 2026-04-15.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.2330407,"width":0.05618351,"height":0.012769354},"value":"Planning 2026-04-15.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"15 Apr 2026 at 11:14","depth":7,"bounds":{"left":0.8656915,"top":0.2330407,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"2,75 GB","depth":7,"bounds":{"left":0.9368351,"top":0.2330407,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.2330407,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Retro 2026-04-14.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.2490024,"width":0.049867023,"height":0.012769354},"value":"Retro 2026-04-14.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"14 Apr 2026 at 17:37","depth":7,"bounds":{"left":0.8656915,"top":0.2490024,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"1,44 GB","depth":7,"bounds":{"left":0.9368351,"top":0.2490024,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.2490024,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-14.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.26496407,"width":0.04886968,"height":0.012769354},"value":"Daily 2026-04-14.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"14 Apr 2026 at 10:09","depth":7,"bounds":{"left":0.8656915,"top":0.26496407,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"924,4 MB","depth":7,"bounds":{"left":0.93351066,"top":0.26496407,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.26496407,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"User pilot (Adi) 2026-04-09.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.28092578,"width":0.07014628,"height":0.012769354},"value":"User pilot (Adi) 2026-04-09.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"9 Apr 2026 at 14:47","depth":7,"bounds":{"left":0.8656915,"top":0.28092578,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"362,6 MB","depth":7,"bounds":{"left":0.93351066,"top":0.28092578,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.28092578,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-09.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.29688746,"width":0.049534574,"height":0.012769354},"value":"Daily 2026-04-09.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"9 Apr 2026 at 10:07","depth":7,"bounds":{"left":0.8656915,"top":0.29688746,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"748,8 MB","depth":7,"bounds":{"left":0.93351066,"top":0.29688746,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.29688746,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-08.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.31284916,"width":0.049534574,"height":0.012769354},"value":"Daily 2026-04-08.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"8 Apr 2026 at 10:13","depth":7,"bounds":{"left":0.8656915,"top":0.31284916,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"1,04 GB","depth":7,"bounds":{"left":0.9368351,"top":0.31284916,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.31284916,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-07.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.32881084,"width":0.04886968,"height":0.012769354},"value":"Daily 2026-04-07.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"7 Apr 2026 at 10:01","depth":7,"bounds":{"left":0.8656915,"top":0.32881084,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"575,5 MB","depth":7,"bounds":{"left":0.93351066,"top":0.32881084,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.32881084,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-06.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.34477255,"width":0.049534574,"height":0.012769354},"value":"Daily 2026-04-06.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"6 Apr 2026 at 10:08","depth":7,"bounds":{"left":0.8656915,"top":0.34477255,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"720,5 MB","depth":7,"bounds":{"left":0.93351066,"top":0.34477255,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.34477255,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-03.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.36073422,"width":0.049534574,"height":0.012769354},"value":"Daily 2026-04-03.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"3 Apr 2026 at 10:21","depth":7,"bounds":{"left":0.8656915,"top":0.36073422,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"1,02 GB","depth":7,"bounds":{"left":0.9368351,"top":0.36073422,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.36073422,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Planning 2026-04-01 & task split.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.37669593,"width":0.080119684,"height":0.012769354},"value":"Planning 2026-04-01 & task split.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"1 Apr 2026 at 12:20","depth":7,"bounds":{"left":0.8656915,"top":0.37669593,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"4,68 GB","depth":7,"bounds":{"left":0.9368351,"top":0.37669593,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.37669593,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Retro 2026-03-31.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.3926576,"width":0.049867023,"height":0.012769354},"value":"Retro 2026-03-31.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"31 Mar 2026 at 18:29","depth":7,"bounds":{"left":0.8656915,"top":0.3926576,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"3,4 GB","depth":7,"bounds":{"left":0.93949467,"top":0.3926576,"width":0.015292553,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.3926576,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-31.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.4086193,"width":0.04886968,"height":0.012769354},"value":"Daily 2026-03-31.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"31 Mar 2026 at 10:10","depth":7,"bounds":{"left":0.8656915,"top":0.4086193,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"923,6 MB","depth":7,"bounds":{"left":0.93351066,"top":0.4086193,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.4086193,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-03-30.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.424581,"width":0.0625,"height":0.012769354},"value":"Refinement 2026-03-30.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"30 Mar 2026 at 17:12","depth":7,"bounds":{"left":0.8656915,"top":0.424581,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"2,77 GB","depth":7,"bounds":{"left":0.9368351,"top":0.424581,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.424581,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-30.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.4405427,"width":0.049534574,"height":0.012769354},"value":"Daily 2026-03-30.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"30 Mar 2026 at 10:05","depth":7,"bounds":{"left":0.8656915,"top":0.4405427,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"641,8 MB","depth":7,"bounds":{"left":0.93351066,"top":0.4405427,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.4405427,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-27.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.45650437,"width":0.04886968,"height":0.012769354},"value":"Daily 2026-03-27.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"27 Mar 2026 at 10:09","depth":7,"bounds":{"left":0.8656915,"top":0.45650437,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"884,3 MB","depth":7,"bounds":{"left":0.93351066,"top":0.45650437,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.45650437,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-26.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.47246608,"width":0.049534574,"height":0.012769354},"value":"Daily 2026-03-26.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"26 Mar 2026 at 9:59","depth":7,"bounds":{"left":0.8656915,"top":0.47246608,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"476,6 MB","depth":7,"bounds":{"left":0.93351066,"top":0.47246608,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.47246608,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-24.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.4884278,"width":0.049534574,"height":0.012769354},"value":"Daily 2026-03-24.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"24 Mar 2026 at 10:00","depth":7,"bounds":{"left":0.8656915,"top":0.4884278,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"550,8 MB","depth":7,"bounds":{"left":0.93351066,"top":0.4884278,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.4884278,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-03-23.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.50438946,"width":0.0625,"height":0.012769354},"value":"Refinement 2026-03-23.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"23 Mar 2026 at 17:03","depth":7,"bounds":{"left":0.8656915,"top":0.50438946,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"3,44 GB","depth":7,"bounds":{"left":0.9368351,"top":0.50438946,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.50438946,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-23.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.5203512,"width":0.049534574,"height":0.012769354},"value":"Daily 2026-03-23.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"23 Mar 2026 at 10:00","depth":7,"bounds":{"left":0.8656915,"top":0.5203512,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"438,9 MB","depth":7,"bounds":{"left":0.93351066,"top":0.5203512,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.5203512,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"BE chapter 2026-03-20.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.5363129,"width":0.061502658,"height":0.012769354},"value":"BE chapter 2026-03-20.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"20 Mar 2026 at 11:46","depth":7,"bounds":{"left":0.8656915,"top":0.5363129,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"1,68 GB","depth":7,"bounds":{"left":0.9368351,"top":0.5363129,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.5363129,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-20.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.5522745,"width":0.049534574,"height":0.012769354},"value":"Daily 2026-03-20.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"20 Mar 2026 at 10:06","depth":7,"bounds":{"left":0.8656915,"top":0.5522745,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"430,4 MB","depth":7,"bounds":{"left":0.93351066,"top":0.5522745,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.5522745,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Planing 2026-03-18-converted.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.56823623,"width":0.076130316,"height":0.012769354},"value":"Planing 2026-03-18-converted.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"19 Mar 2026 at 12:01","depth":7,"bounds":{"left":0.8656915,"top":0.56823623,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"2,38 GB","depth":7,"bounds":{"left":0.9368351,"top":0.56823623,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.56823623,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-02-09-converted.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.58419794,"width":0.084773935,"height":0.012769354},"value":"Refinement 2026-02-09-converted.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"19 Mar 2026 at 11:35","depth":7,"bounds":{"left":0.8656915,"top":0.58419794,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"2,26 GB","depth":7,"bounds":{"left":0.9368351,"top":0.58419794,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.58419794,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-19.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.60015965,"width":0.04886968,"height":0.012769354},"value":"Daily 2026-03-19.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"19 Mar 2026 at 9:57","depth":7,"bounds":{"left":0.8656915,"top":0.60015965,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"386,3 MB","depth":7,"bounds":{"left":0.93351066,"top":0.60015965,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.60015965,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Review 2026-03-18.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.6161213,"width":0.05319149,"height":0.012769354},"value":"Review 2026-03-18.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"18 Mar 2026 at 16:20","depth":7,"bounds":{"left":0.8656915,"top":0.6161213,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"705,8 MB","depth":7,"bounds":{"left":0.93351066,"top":0.6161213,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.6161213,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Planing 2026-03-18.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.632083,"width":0.053856384,"height":0.012769354},"value":"Planing 2026-03-18.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"18 Mar 2026 at 11:14","depth":7,"bounds":{"left":0.8656915,"top":0.632083,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"2,78 GB","depth":7,"bounds":{"left":0.9368351,"top":0.632083,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.632083,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Retro 2026-03-17.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.6480447,"width":0.04920213,"height":0.012769354},"value":"Retro 2026-03-17.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"17 Mar 2026 at 17:40","depth":7,"bounds":{"left":0.8656915,"top":0.6480447,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"1,53 GB","depth":7,"bounds":{"left":0.9368351,"top":0.6480447,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.6480447,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-17.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.6640064,"width":0.048204787,"height":0.012769354},"value":"Daily 2026-03-17.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"17 Mar 2026 at 10:18","depth":7,"bounds":{"left":0.8656915,"top":0.6640064,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"1,2 GB","depth":7,"bounds":{"left":0.93949467,"top":0.6640064,"width":0.015292553,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.6640064,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-03-16.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.67996806,"width":0.061835106,"height":0.012769354},"value":"Refinement 2026-03-16.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"16 Mar 2026 at 16:55","depth":7,"bounds":{"left":0.8656915,"top":0.67996806,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"4,19 GB","depth":7,"bounds":{"left":0.9368351,"top":0.67996806,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.67996806,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-16.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.69592977,"width":0.04886968,"height":0.012769354},"value":"Daily 2026-03-16.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"16 Mar 2026 at 10:02","depth":7,"bounds":{"left":0.8656915,"top":0.69592977,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"592,2 MB","depth":7,"bounds":{"left":0.93351066,"top":0.69592977,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.69592977,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-13.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.7118915,"width":0.04886968,"height":0.012769354},"value":"Daily 2026-03-13.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"13 Mar 2026 at 10:12","depth":7,"bounds":{"left":0.8656915,"top":0.7118915,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"1,02 GB","depth":7,"bounds":{"left":0.9368351,"top":0.7118915,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.7118915,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"1-1 2026-03-12.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.7278532,"width":0.04488032,"height":0.012769354},"value":"1-1 2026-03-12.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"12 Mar 2026 at 18:35","depth":7,"bounds":{"left":0.8656915,"top":0.7278532,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"637,6 MB","depth":7,"bounds":{"left":0.93351066,"top":0.7278532,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.7278532,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-12.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.7438148,"width":0.04886968,"height":0.012769354},"value":"Daily 2026-03-12.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"12 Mar 2026 at 10:10","depth":7,"bounds":{"left":0.8656915,"top":0.7438148,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"978,7 MB","depth":7,"bounds":{"left":0.93351066,"top":0.7438148,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.7438148,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-11.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.75977653,"width":0.048204787,"height":0.012769354},"value":"Daily 2026-03-11.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"11 Mar 2026 at 10:06","depth":7,"bounds":{"left":0.8656915,"top":0.75977653,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"798,7 MB","depth":7,"bounds":{"left":0.93351066,"top":0.75977653,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.75977653,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-10.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.77573824,"width":0.04886968,"height":0.012769354},"value":"Daily 2026-03-10.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"10 Mar 2026 at 9:57","depth":7,"bounds":{"left":0.8656915,"top":0.77573824,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"404,6 MB","depth":7,"bounds":{"left":0.93351066,"top":0.77573824,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.77573824,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-03-09.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.79169995,"width":0.0625,"height":0.012769354},"value":"Refinement 2026-03-09.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"9 Mar 2026 at 17:04","depth":7,"bounds":{"left":0.8656915,"top":0.79169995,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"4,16 GB","depth":7,"bounds":{"left":0.9368351,"top":0.79169995,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.79169995,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-09.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.8076616,"width":0.049534574,"height":0.012769354},"value":"Daily 2026-03-09.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"9 Mar 2026 at 9:56","depth":7,"bounds":{"left":0.8656915,"top":0.8076616,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"319,7 MB","depth":7,"bounds":{"left":0.93351066,"top":0.8076616,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.8076616,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-06.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.8236233,"width":0.049534574,"height":0.012769354},"value":"Daily 2026-03-06.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"6 Mar 2026 at 9:57","depth":7,"bounds":{"left":0.8656915,"top":0.8236233,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"291,7 MB","depth":7,"bounds":{"left":0.93351066,"top":0.8236233,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.8236233,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Planning 2026-03-04.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.839585,"width":0.056848403,"height":0.012769354},"value":"Planning 2026-03-04.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 Mar 2026 at 11:09","depth":7,"bounds":{"left":0.8656915,"top":0.839585,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"2,62 GB","depth":7,"bounds":{"left":0.9368351,"top":0.839585,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.839585,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-02.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.8555467,"width":0.049534574,"height":0.012769354},"value":"Daily 2026-03-02.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"2 Mar 2026 at 10:07","depth":7,"bounds":{"left":0.8656915,"top":0.8555467,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"768,5 MB","depth":7,"bounds":{"left":0.93351066,"top":0.8555467,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.8555467,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-02-27.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.87150836,"width":0.04886968,"height":0.012769354},"value":"Daily 2026-02-27.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"27 Feb 2026 at 10:02","depth":7,"bounds":{"left":0.8656915,"top":0.87150836,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"546,8 MB","depth":7,"bounds":{"left":0.93351066,"top":0.87150836,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.87150836,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-02-26.mov","depth":7,"bounds":{"left":0.58776593,"top":0.88747007,"width":0.04886968,"height":0.012769354},"value":"Daily 2026-02-26.mov","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"26 Feb 2026 at 9:53","depth":7,"bounds":{"left":0.8656915,"top":0.88747007,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"96,6 MB","depth":7,"bounds":{"left":0.93650264,"top":0.88747007,"width":0.01861702,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"QT movie","depth":7,"bounds":{"left":0.9581117,"top":0.88747007,"width":0.034906916,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-02-25.mov","depth":7,"bounds":{"left":0.58776593,"top":0.9034318,"width":0.048537236,"height":0.012769354},"value":"Daily 2026-02-25.mov","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"25 Feb 2026 at 9:59","depth":7,"bounds":{"left":0.8656915,"top":0.9034318,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"503,5 MB","depth":7,"bounds":{"left":0.93351066,"top":0.9034318,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"QT movie","depth":7,"bounds":{"left":0.9581117,"top":0.9034318,"width":0.034906916,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Opportunity-Contacts 2026-02-24.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.9193935,"width":0.08377659,"height":0.012769354},"value":"Opportunity-Contacts 2026-02-24.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"24 Feb 2026 at 12:03","depth":7,"bounds":{"left":0.8656915,"top":0.9193935,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"791,7 MB","depth":7,"bounds":{"left":0.93351066,"top":0.9193935,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.9193935,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-02-24.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.9353551,"width":0.04920213,"height":0.012769354},"value":"Daily 2026-02-24.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"24 Feb 2026 at 10:02","depth":7,"bounds":{"left":0.8656915,"top":0.9353551,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"520,7 MB","depth":7,"bounds":{"left":0.93351066,"top":0.9353551,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.9353551,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-02-23.mov","depth":7,"bounds":{"left":0.58776593,"top":0.95131683,"width":0.061835106,"height":0.012769354},"value":"Refinement 2026-02-23.mov","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"23 Feb 2026 at 16:31","depth":7,"bounds":{"left":0.8656915,"top":0.95131683,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"2 GB","depth":7,"bounds":{"left":0.94348407,"top":0.95131683,"width":0.011303191,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"QT movie","depth":7,"bounds":{"left":0.9581117,"top":0.95131683,"width":0.034906916,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-02-20 & Ani.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.96727854,"width":0.061170213,"height":0.012769354},"value":"Daily 2026-02-20 & Ani.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"20 Feb 2026 at 10:53","depth":7,"bounds":{"left":0.8656915,"top":0.96727854,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"2,52 GB","depth":7,"bounds":{"left":0.9368351,"top":0.96727854,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.96727854,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-02-19.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.98324025,"width":0.04886968,"height":0.012769354},"value":"Daily 2026-02-19.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"19 Feb 2026 at 9:54","depth":7,"bounds":{"left":0.8656915,"top":0.98324025,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"234,2 MB","depth":7,"bounds":{"left":0.93351066,"top":0.98324025,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.98324025,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Review 2026-02-18.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.9992019,"width":0.05319149,"height":0.0007981062},"value":"Review 2026-02-18.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"18 Feb 2026 at 16:25","depth":7,"bounds":{"left":0.8656915,"top":0.9992019,"width":0.056848403,"height":0.0007981062},"role_description":"text"},{"role":"AXStaticText","text":"925,1 MB","depth":7,"bounds":{"left":0.93351066,"top":0.9992019,"width":0.021276595,"height":0.0007981062},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.9992019,"width":0.032912236,"height":0.0007981062},"role_description":"text"},{"role":"AXTextField","text":"Planning 2026-02-18.mp4","depth":7,"bounds":{"left":0.58776593,"top":1.0,"width":0.05618351,"height":-0.01516366},"value":"Planning 2026-02-18.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"18 Feb 2026 at 10:57","depth":7,"bounds":{"left":0.8656915,"top":1.0,"width":0.056848403,"height":-0.01516366},"role_description":"text"},{"role":"AXStaticText","text":"2,04 GB","depth":7,"bounds":{"left":0.9368351,"top":1.0,"width":0.017952127,"height":-0.01516366},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":1.0,"width":0.032912236,"height":-0.01516366},"role_description":"text"},{"role":"AXTextField","text":"Retro 2026-02-17.mp4","depth":7,"bounds":{"left":0.58776593,"top":1.0,"width":0.05618351,"height":-0.031125307},"value":"Retro 2026-02-17.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"17 Feb 2026 at 17:38","depth":7,"bounds":{"left":0.8656915,"top":1.0,"width":0.056848403,"height":-0.031125307},"role_description":"text"},{"role":"AXStaticText","text":"1,31 GB","depth":7,"bounds":{"left":0.9368351,"top":1.0,"width":0.017952127,"height":-0.031125307},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":1.0,"width":0.032912236,"height":-0.031125307},"role_description":"text"},{"role":"AXTextField","text":"Refinement & P1 debugging 2026-02-16.mp4","depth":7,"bounds":{"left":0.58776593,"top":1.0,"width":0.09474734,"height":-0.047086954},"value":"Refinement & P1 debugging 2026-02-16.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"16 Feb 2026 at 17:36","depth":7,"bounds":{"left":0.8656915,"top":1.0,"width":0.056848403,"height":-0.047086954},"role_description":"text"},{"role":"AXStaticText","text":"4,53 GB","depth":7,"bounds":{"left":0.9368351,"top":1.0,"width":0.017952127,"height":-0.047086954},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":1.0,"width":0.032912236,"height":-0.047086954},"role_description":"text"},{"role":"AXTextField","text":"SyncObjects3 2026-02-16.mp4","depth":7,"bounds":{"left":0.58776593,"top":1.0,"width":0.09474734,"height":-0.06304872},"value":"SyncObjects3 2026-02-16.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
-8656069768220031265
|
-2910781466942463541
|
idle
|
accessibility
|
NULL
|
Favourites
jiminny
AirDrop
Recents
Applications
Do Favourites
jiminny
AirDrop
Recents
Applications
Documents
Downloads
lukas
iCloud
iCloud Drive
Sync folder
Locations
DXP4800PLUS-B5F
Eject
Network
Tags
CRM
Orange
Red
Yellow
Green
Blue
Purple
All Tags…
Name
Date Modified
Size
Kind
2026
Today at 10:32
--
Folder
CleanShot 2026-04-23 at 09.45.36.mp4
Today at 10:32
1,74 GB
MPEG-4 movie
Daily 2026-04-22.mp4
Yesterday at 10:21
1,36 GB
MPEG-4 movie
Refinement 2026-04-06.mp4
21 Apr 2026 at 11:02
2,41 GB
MPEG-4 movie
Daily 2026-04-21.mp4
21 Apr 2026 at 10:00
567,8 MB
MPEG-4 movie
Refinement 2026-04-20.mp4
20 Apr 2026 at 16:56
4,25 GB
MPEG-4 movie
Daily 2026-04-20.mp4
20 Apr 2026 at 10:06
698,5 MB
MPEG-4 movie
Daily 2026-04-17.mp4
17 Apr 2026 at 10:16
1,16 GB
MPEG-4 movie
Daily 2026-04-16.mp4
16 Apr 2026 at 10:00
513,4 MB
MPEG-4 movie
Planning 2026-04-15.mp4
15 Apr 2026 at 11:14
2,75 GB
MPEG-4 movie
Retro 2026-04-14.mp4
14 Apr 2026 at 17:37
1,44 GB
MPEG-4 movie
Daily 2026-04-14.mp4
14 Apr 2026 at 10:09
924,4 MB
MPEG-4 movie
User pilot (Adi) 2026-04-09.mp4
9 Apr 2026 at 14:47
362,6 MB
MPEG-4 movie
Daily 2026-04-09.mp4
9 Apr 2026 at 10:07
748,8 MB
MPEG-4 movie
Daily 2026-04-08.mp4
8 Apr 2026 at 10:13
1,04 GB
MPEG-4 movie
Daily 2026-04-07.mp4
7 Apr 2026 at 10:01
575,5 MB
MPEG-4 movie
Daily 2026-04-06.mp4
6 Apr 2026 at 10:08
720,5 MB
MPEG-4 movie
Daily 2026-04-03.mp4
3 Apr 2026 at 10:21
1,02 GB
MPEG-4 movie
Planning 2026-04-01 & task split.mp4
1 Apr 2026 at 12:20
4,68 GB
MPEG-4 movie
Retro 2026-03-31.mp4
31 Mar 2026 at 18:29
3,4 GB
MPEG-4 movie
Daily 2026-03-31.mp4
31 Mar 2026 at 10:10
923,6 MB
MPEG-4 movie
Refinement 2026-03-30.mp4
30 Mar 2026 at 17:12
2,77 GB
MPEG-4 movie
Daily 2026-03-30.mp4
30 Mar 2026 at 10:05
641,8 MB
MPEG-4 movie
Daily 2026-03-27.mp4
27 Mar 2026 at 10:09
884,3 MB
MPEG-4 movie
Daily 2026-03-26.mp4
26 Mar 2026 at 9:59
476,6 MB
MPEG-4 movie
Daily 2026-03-24.mp4
24 Mar 2026 at 10:00
550,8 MB
MPEG-4 movie
Refinement 2026-03-23.mp4
23 Mar 2026 at 17:03
3,44 GB
MPEG-4 movie
Daily 2026-03-23.mp4
23 Mar 2026 at 10:00
438,9 MB
MPEG-4 movie
BE chapter 2026-03-20.mp4
20 Mar 2026 at 11:46
1,68 GB
MPEG-4 movie
Daily 2026-03-20.mp4
20 Mar 2026 at 10:06
430,4 MB
MPEG-4 movie
Planing 2026-03-18-converted.mp4
19 Mar 2026 at 12:01
2,38 GB
MPEG-4 movie
Refinement 2026-02-09-converted.mp4
19 Mar 2026 at 11:35
2,26 GB
MPEG-4 movie
Daily 2026-03-19.mp4
19 Mar 2026 at 9:57
386,3 MB
MPEG-4 movie
Review 2026-03-18.mp4
18 Mar 2026 at 16:20
705,8 MB
MPEG-4 movie
Planing 2026-03-18.mp4
18 Mar 2026 at 11:14
2,78 GB
MPEG-4 movie
Retro 2026-03-17.mp4
17 Mar 2026 at 17:40
1,53 GB
MPEG-4 movie
Daily 2026-03-17.mp4
17 Mar 2026 at 10:18
1,2 GB
MPEG-4 movie
Refinement 2026-03-16.mp4
16 Mar 2026 at 16:55
4,19 GB
MPEG-4 movie
Daily 2026-03-16.mp4
16 Mar 2026 at 10:02
592,2 MB
MPEG-4 movie
Daily 2026-03-13.mp4
13 Mar 2026 at 10:12
1,02 GB
MPEG-4 movie
1-1 2026-03-12.mp4
12 Mar 2026 at 18:35
637,6 MB
MPEG-4 movie
Daily 2026-03-12.mp4
12 Mar 2026 at 10:10
978,7 MB
MPEG-4 movie
Daily 2026-03-11.mp4
11 Mar 2026 at 10:06
798,7 MB
MPEG-4 movie
Daily 2026-03-10.mp4
10 Mar 2026 at 9:57
404,6 MB
MPEG-4 movie
Refinement 2026-03-09.mp4
9 Mar 2026 at 17:04
4,16 GB
MPEG-4 movie
Daily 2026-03-09.mp4
9 Mar 2026 at 9:56
319,7 MB
MPEG-4 movie
Daily 2026-03-06.mp4
6 Mar 2026 at 9:57
291,7 MB
MPEG-4 movie
Planning 2026-03-04.mp4
4 Mar 2026 at 11:09
2,62 GB
MPEG-4 movie
Daily 2026-03-02.mp4
2 Mar 2026 at 10:07
768,5 MB
MPEG-4 movie
Daily 2026-02-27.mp4
27 Feb 2026 at 10:02
546,8 MB
MPEG-4 movie
Daily 2026-02-26.mov
26 Feb 2026 at 9:53
96,6 MB
QT movie
Daily 2026-02-25.mov
25 Feb 2026 at 9:59
503,5 MB
QT movie
Opportunity-Contacts 2026-02-24.mp4
24 Feb 2026 at 12:03
791,7 MB
MPEG-4 movie
Daily 2026-02-24.mp4
24 Feb 2026 at 10:02
520,7 MB
MPEG-4 movie
Refinement 2026-02-23.mov
23 Feb 2026 at 16:31
2 GB
QT movie
Daily 2026-02-20 & Ani.mp4
20 Feb 2026 at 10:53
2,52 GB
MPEG-4 movie
Daily 2026-02-19.mp4
19 Feb 2026 at 9:54
234,2 MB
MPEG-4 movie
Review 2026-02-18.mp4
18 Feb 2026 at 16:25
925,1 MB
MPEG-4 movie
Planning 2026-02-18.mp4
18 Feb 2026 at 10:57
2,04 GB
MPEG-4 movie
Retro 2026-02-17.mp4
17 Feb 2026 at 17:38
1,31 GB
MPEG-4 movie
Refinement & P1 debugging 2026-02-16.mp4
16 Feb 2026 at 17:36
4,53 GB
MPEG-4 movie
SyncObjects3 2026-02-16.mp4...
|
NULL
|
|
73441
|
NULL
|
0
|
2026-04-23T07:41:04.512812+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776930064512_m1.jpg...
|
Finder
|
Work
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Favourites
jiminny
AirDrop
Recents
Applications
Do Favourites
jiminny
AirDrop
Recents
Applications
Documents
Downloads
lukas
iCloud
iCloud Drive
Sync folder
Locations
DXP4800PLUS-B5F
Eject
Network
Tags
CRM
Orange
Red
Yellow
Green
Blue
Purple
All Tags…
Name
Date Modified
Size
Kind
2026
Today at 10:32
--
Folder
CleanShot 2026-04-23 at 09.45.36.mp4
Today at 10:32
1,74 GB
MPEG-4 movie
Daily 2026-04-22.mp4
Yesterday at 10:21
1,36 GB
MPEG-4 movie
Refinement 2026-04-06.mp4
21 Apr 2026 at 11:02
2,41 GB
MPEG-4 movie
Daily 2026-04-21.mp4
21 Apr 2026 at 10:00
567,8 MB
MPEG-4 movie
Refinement 2026-04-20.mp4
20 Apr 2026 at 16:56
4,25 GB
MPEG-4 movie
Daily 2026-04-20.mp4
20 Apr 2026 at 10:06
698,5 MB
MPEG-4 movie
Daily 2026-04-17.mp4
17 Apr 2026 at 10:16
1,16 GB
MPEG-4 movie
Daily 2026-04-16.mp4
16 Apr 2026 at 10:00
513,4 MB
MPEG-4 movie
Planning 2026-04-15.mp4
15 Apr 2026 at 11:14
2,75 GB
MPEG-4 movie
Retro 2026-04-14.mp4
14 Apr 2026 at 17:37
1,44 GB
MPEG-4 movie
Daily 2026-04-14.mp4
14 Apr 2026 at 10:09
924,4 MB
MPEG-4 movie
User pilot (Adi) 2026-04-09.mp4
9 Apr 2026 at 14:47
362,6 MB
MPEG-4 movie
Daily 2026-04-09.mp4
9 Apr 2026 at 10:07
748,8 MB
MPEG-4 movie
Daily 2026-04-08.mp4
8 Apr 2026 at 10:13
1,04 GB
MPEG-4 movie
Daily 2026-04-07.mp4
7 Apr 2026 at 10:01
575,5 MB
MPEG-4 movie
Daily 2026-04-06.mp4
6 Apr 2026 at 10:08
720,5 MB
MPEG-4 movie
Daily 2026-04-03.mp4
3 Apr 2026 at 10:21
1,02 GB
MPEG-4 movie
Planning 2026-04-01 & task split.mp4
1 Apr 2026 at 12:20
4,68 GB
MPEG-4 movie
Retro 2026-03-31.mp4
31 Mar 2026 at 18:29
3,4 GB
MPEG-4 movie
Daily 2026-03-31.mp4
31 Mar 2026 at 10:10
923,6 MB
MPEG-4 movie
Refinement 2026-03-30.mp4
30 Mar 2026 at 17:12
2,77 GB
MPEG-4 movie
Daily 2026-03-30.mp4
30 Mar 2026 at 10:05
641,8 MB
MPEG-4 movie
Daily 2026-03-27.mp4
27 Mar 2026 at 10:09
884,3 MB
MPEG-4 movie
Daily 2026-03-26.mp4
26 Mar 2026 at 9:59
476,6 MB
MPEG-4 movie
Daily 2026-03-24.mp4
24 Mar 2026 at 10:00
550,8 MB
MPEG-4 movie
Refinement 2026-03-23.mp4
23 Mar 2026 at 17:03
3,44 GB
MPEG-4 movie
Daily 2026-03-23.mp4
23 Mar 2026 at 10:00
438,9 MB
MPEG-4 movie
BE chapter 2026-03-20.mp4
20 Mar 2026 at 11:46
1,68 GB
MPEG-4 movie
Daily 2026-03-20.mp4
20 Mar 2026 at 10:06
430,4 MB
MPEG-4 movie
Planing 2026-03-18-converted.mp4
19 Mar 2026 at 12:01
2,38 GB
MPEG-4 movie
Refinement 2026-02-09-converted.mp4
19 Mar 2026 at 11:35
2,26 GB
MPEG-4 movie
Daily 2026-03-19.mp4
19 Mar 2026 at 9:57
386,3 MB
MPEG-4 movie
Review 2026-03-18.mp4
18 Mar 2026 at 16:20
705,8 MB
MPEG-4 movie
Planing 2026-03-18.mp4
18 Mar 2026 at 11:14
2,78 GB
MPEG-4 movie
Retro 2026-03-17.mp4
17 Mar 2026 at 17:40
1,53 GB
MPEG-4 movie
Daily 2026-03-17.mp4
17 Mar 2026 at 10:18
1,2 GB
MPEG-4 movie
Refinement 2026-03-16.mp4
16 Mar 2026 at 16:55
4,19 GB
MPEG-4 movie
Daily 2026-03-16.mp4
16 Mar 2026 at 10:02
592,2 MB
MPEG-4 movie
Daily 2026-03-13.mp4
13 Mar 2026 at 10:12
1,02 GB
MPEG-4 movie
1-1 2026-03-12.mp4
12 Mar 2026 at 18:35
637,6 MB
MPEG-4 movie
Daily 2026-03-12.mp4
12 Mar 2026 at 10:10
978,7 MB
MPEG-4 movie
Daily 2026-03-11.mp4
11 Mar 2026 at 10:06
798,7 MB
MPEG-4 movie
Daily 2026-03-10.mp4
10 Mar 2026 at 9:57
404,6 MB
MPEG-4 movie
Refinement 2026-03-09.mp4
9 Mar 2026 at 17:04
4,16 GB
MPEG-4 movie
Daily 2026-03-09.mp4
9 Mar 2026 at 9:56
319,7 MB
MPEG-4 movie
Daily 2026-03-06.mp4
6 Mar 2026 at 9:57
291,7 MB
MPEG-4 movie
Planning 2026-03-04.mp4
4 Mar 2026 at 11:09
2,62 GB
MPEG-4 movie
Daily 2026-03-02.mp4
2 Mar 2026 at 10:07
768,5 MB
MPEG-4 movie
Daily 2026-02-27.mp4
27 Feb 2026 at 10:02
546,8 MB
MPEG-4 movie
Daily 2026-02-26.mov
26 Feb 2026 at 9:53
96,6 MB
QT movie
Daily 2026-02-25.mov
25 Feb 2026 at 9:59
503,5 MB
QT movie
Opportunity-Contacts 2026-02-24.mp4
24 Feb 2026 at 12:03
791,7 MB
MPEG-4 movie
Daily 2026-02-24.mp4
24 Feb 2026 at 10:02
520,7 MB
MPEG-4 movie
Refinement 2026-02-23.mov
23 Feb 2026 at 16:31
2 GB
QT movie
Daily 2026-02-20 & Ani.mp4
20 Feb 2026 at 10:53
2,52 GB
MPEG-4 movie
Daily 2026-02-19.mp4
19 Feb 2026 at 9:54
234,2 MB
MPEG-4 movie
Review 2026-02-18.mp4
18 Feb 2026 at 16:25
925,1 MB
MPEG-4 movie
Planning 2026-02-18.mp4
18 Feb 2026 at 10:57
2,04 GB
MPEG-4 movie
Retro 2026-02-17.mp4
17 Feb 2026 at 17:38
1,31 GB
MPEG-4 movie
Refinement & P1 debugging 2026-02-16.mp4
16 Feb 2026 at 17:36
4,53 GB
MPEG-4 movie...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Favourites","depth":6,"automation_id":"xSidebarHeader","role_description":"text"},{"role":"AXStaticText","text":"jiminny","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"AirDrop","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"Recents","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"Applications","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"Documents","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"Downloads","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"lukas","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"iCloud","depth":6,"automation_id":"xSidebarHeader","role_description":"text"},{"role":"AXStaticText","text":"iCloud Drive","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"Sync folder","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"Locations","depth":6,"automation_id":"xSidebarHeader","role_description":"text"},{"role":"AXStaticText","text":"DXP4800PLUS-B5F","depth":6,"role_description":"text"},{"role":"AXButton","text":"Eject","depth":6,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"Network","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"Tags","depth":6,"automation_id":"xSidebarHeader","role_description":"text"},{"role":"AXStaticText","text":"CRM","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"Orange","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"Red","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"Yellow","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"Green","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"Blue","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"Purple","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"All Tags…","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"Name","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"Date Modified","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"Size","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"Kind","depth":7,"role_description":"text"},{"role":"AXTextField","text":"2026","depth":7,"value":"2026","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Today at 10:32","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"--","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"Folder","depth":7,"role_description":"text"},{"role":"AXTextField","text":"CleanShot 2026-04-23 at 09.45.36.mp4","depth":7,"value":"CleanShot 2026-04-23 at 09.45.36.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Today at 10:32","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"1,74 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-22.mp4","depth":7,"value":"Daily 2026-04-22.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Yesterday at 10:21","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"1,36 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-04-06.mp4","depth":7,"value":"Refinement 2026-04-06.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"21 Apr 2026 at 11:02","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"2,41 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-21.mp4","depth":7,"value":"Daily 2026-04-21.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"21 Apr 2026 at 10:00","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"567,8 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-04-20.mp4","depth":7,"value":"Refinement 2026-04-20.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"20 Apr 2026 at 16:56","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"4,25 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-20.mp4","depth":7,"value":"Daily 2026-04-20.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"20 Apr 2026 at 10:06","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"698,5 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-17.mp4","depth":7,"value":"Daily 2026-04-17.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"17 Apr 2026 at 10:16","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"1,16 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-16.mp4","depth":7,"value":"Daily 2026-04-16.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"16 Apr 2026 at 10:00","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"513,4 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Planning 2026-04-15.mp4","depth":7,"value":"Planning 2026-04-15.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"15 Apr 2026 at 11:14","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"2,75 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Retro 2026-04-14.mp4","depth":7,"value":"Retro 2026-04-14.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"14 Apr 2026 at 17:37","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"1,44 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-14.mp4","depth":7,"value":"Daily 2026-04-14.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"14 Apr 2026 at 10:09","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"924,4 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"User pilot (Adi) 2026-04-09.mp4","depth":7,"value":"User pilot (Adi) 2026-04-09.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"9 Apr 2026 at 14:47","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"362,6 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-09.mp4","depth":7,"value":"Daily 2026-04-09.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"9 Apr 2026 at 10:07","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"748,8 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-08.mp4","depth":7,"value":"Daily 2026-04-08.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"8 Apr 2026 at 10:13","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"1,04 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-07.mp4","depth":7,"value":"Daily 2026-04-07.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"7 Apr 2026 at 10:01","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"575,5 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-06.mp4","depth":7,"value":"Daily 2026-04-06.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"6 Apr 2026 at 10:08","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"720,5 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-03.mp4","depth":7,"value":"Daily 2026-04-03.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"3 Apr 2026 at 10:21","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"1,02 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Planning 2026-04-01 & task split.mp4","depth":7,"value":"Planning 2026-04-01 & task split.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"1 Apr 2026 at 12:20","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"4,68 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Retro 2026-03-31.mp4","depth":7,"value":"Retro 2026-03-31.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"31 Mar 2026 at 18:29","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"3,4 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-31.mp4","depth":7,"value":"Daily 2026-03-31.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"31 Mar 2026 at 10:10","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"923,6 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-03-30.mp4","depth":7,"value":"Refinement 2026-03-30.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"30 Mar 2026 at 17:12","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"2,77 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-30.mp4","depth":7,"value":"Daily 2026-03-30.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"30 Mar 2026 at 10:05","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"641,8 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-27.mp4","depth":7,"value":"Daily 2026-03-27.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"27 Mar 2026 at 10:09","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"884,3 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-26.mp4","depth":7,"value":"Daily 2026-03-26.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"26 Mar 2026 at 9:59","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"476,6 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-24.mp4","depth":7,"value":"Daily 2026-03-24.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"24 Mar 2026 at 10:00","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"550,8 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-03-23.mp4","depth":7,"value":"Refinement 2026-03-23.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"23 Mar 2026 at 17:03","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"3,44 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-23.mp4","depth":7,"value":"Daily 2026-03-23.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"23 Mar 2026 at 10:00","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"438,9 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"BE chapter 2026-03-20.mp4","depth":7,"value":"BE chapter 2026-03-20.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"20 Mar 2026 at 11:46","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"1,68 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-20.mp4","depth":7,"value":"Daily 2026-03-20.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"20 Mar 2026 at 10:06","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"430,4 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Planing 2026-03-18-converted.mp4","depth":7,"value":"Planing 2026-03-18-converted.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"19 Mar 2026 at 12:01","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"2,38 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-02-09-converted.mp4","depth":7,"value":"Refinement 2026-02-09-converted.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"19 Mar 2026 at 11:35","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"2,26 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-19.mp4","depth":7,"value":"Daily 2026-03-19.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"19 Mar 2026 at 9:57","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"386,3 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Review 2026-03-18.mp4","depth":7,"value":"Review 2026-03-18.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"18 Mar 2026 at 16:20","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"705,8 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Planing 2026-03-18.mp4","depth":7,"value":"Planing 2026-03-18.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"18 Mar 2026 at 11:14","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"2,78 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Retro 2026-03-17.mp4","depth":7,"value":"Retro 2026-03-17.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"17 Mar 2026 at 17:40","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"1,53 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-17.mp4","depth":7,"value":"Daily 2026-03-17.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"17 Mar 2026 at 10:18","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"1,2 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-03-16.mp4","depth":7,"value":"Refinement 2026-03-16.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"16 Mar 2026 at 16:55","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"4,19 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-16.mp4","depth":7,"value":"Daily 2026-03-16.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"16 Mar 2026 at 10:02","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"592,2 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-13.mp4","depth":7,"value":"Daily 2026-03-13.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"13 Mar 2026 at 10:12","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"1,02 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"1-1 2026-03-12.mp4","depth":7,"value":"1-1 2026-03-12.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"12 Mar 2026 at 18:35","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"637,6 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-12.mp4","depth":7,"value":"Daily 2026-03-12.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"12 Mar 2026 at 10:10","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"978,7 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-11.mp4","depth":7,"value":"Daily 2026-03-11.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"11 Mar 2026 at 10:06","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"798,7 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-10.mp4","depth":7,"value":"Daily 2026-03-10.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"10 Mar 2026 at 9:57","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"404,6 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-03-09.mp4","depth":7,"value":"Refinement 2026-03-09.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"9 Mar 2026 at 17:04","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"4,16 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-09.mp4","depth":7,"value":"Daily 2026-03-09.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"9 Mar 2026 at 9:56","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"319,7 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-06.mp4","depth":7,"value":"Daily 2026-03-06.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"6 Mar 2026 at 9:57","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"291,7 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Planning 2026-03-04.mp4","depth":7,"value":"Planning 2026-03-04.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 Mar 2026 at 11:09","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"2,62 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-02.mp4","depth":7,"value":"Daily 2026-03-02.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"2 Mar 2026 at 10:07","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"768,5 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-02-27.mp4","depth":7,"value":"Daily 2026-02-27.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"27 Feb 2026 at 10:02","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"546,8 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-02-26.mov","depth":7,"value":"Daily 2026-02-26.mov","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"26 Feb 2026 at 9:53","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"96,6 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"QT movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-02-25.mov","depth":7,"value":"Daily 2026-02-25.mov","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"25 Feb 2026 at 9:59","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"503,5 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"QT movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Opportunity-Contacts 2026-02-24.mp4","depth":7,"value":"Opportunity-Contacts 2026-02-24.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"24 Feb 2026 at 12:03","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"791,7 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-02-24.mp4","depth":7,"bounds":{"left":0.6631944,"top":0.0,"width":0.10277778,"height":0.017777778},"value":"Daily 2026-02-24.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"24 Feb 2026 at 10:02","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"520,7 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-02-23.mov","depth":7,"bounds":{"left":0.6631944,"top":0.0,"width":0.12916666,"height":0.017777778},"value":"Refinement 2026-02-23.mov","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"23 Feb 2026 at 16:31","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"2 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"QT movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-02-20 & Ani.mp4","depth":7,"bounds":{"left":0.6631944,"top":0.0,"width":0.12777779,"height":0.017777778},"value":"Daily 2026-02-20 & Ani.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"20 Feb 2026 at 10:53","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"2,52 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-02-19.mp4","depth":7,"bounds":{"left":0.6631944,"top":0.0,"width":0.10208333,"height":0.017777778},"value":"Daily 2026-02-19.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"19 Feb 2026 at 9:54","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"234,2 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Review 2026-02-18.mp4","depth":7,"bounds":{"left":0.6631944,"top":0.0,"width":0.11111111,"height":0.017777778},"value":"Review 2026-02-18.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"18 Feb 2026 at 16:25","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"925,1 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Planning 2026-02-18.mp4","depth":7,"bounds":{"left":0.6631944,"top":0.02111111,"width":0.11736111,"height":0.017777778},"value":"Planning 2026-02-18.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"18 Feb 2026 at 10:57","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"2,04 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Retro 2026-02-17.mp4","depth":7,"bounds":{"left":0.6631944,"top":0.043333333,"width":0.11736111,"height":0.017777778},"value":"Retro 2026-02-17.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"17 Feb 2026 at 17:38","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"1,31 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Refinement & P1 debugging 2026-02-16.mp4","depth":7,"bounds":{"left":0.6631944,"top":0.06555556,"width":0.10208333,"height":0.017777778},"value":"Refinement & P1 debugging 2026-02-16.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"16 Feb 2026 at 17:36","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"4,53 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"}]...
|
7072094695368023128
|
-2910781466942447157
|
visual_change
|
accessibility
|
NULL
|
Favourites
jiminny
AirDrop
Recents
Applications
Do Favourites
jiminny
AirDrop
Recents
Applications
Documents
Downloads
lukas
iCloud
iCloud Drive
Sync folder
Locations
DXP4800PLUS-B5F
Eject
Network
Tags
CRM
Orange
Red
Yellow
Green
Blue
Purple
All Tags…
Name
Date Modified
Size
Kind
2026
Today at 10:32
--
Folder
CleanShot 2026-04-23 at 09.45.36.mp4
Today at 10:32
1,74 GB
MPEG-4 movie
Daily 2026-04-22.mp4
Yesterday at 10:21
1,36 GB
MPEG-4 movie
Refinement 2026-04-06.mp4
21 Apr 2026 at 11:02
2,41 GB
MPEG-4 movie
Daily 2026-04-21.mp4
21 Apr 2026 at 10:00
567,8 MB
MPEG-4 movie
Refinement 2026-04-20.mp4
20 Apr 2026 at 16:56
4,25 GB
MPEG-4 movie
Daily 2026-04-20.mp4
20 Apr 2026 at 10:06
698,5 MB
MPEG-4 movie
Daily 2026-04-17.mp4
17 Apr 2026 at 10:16
1,16 GB
MPEG-4 movie
Daily 2026-04-16.mp4
16 Apr 2026 at 10:00
513,4 MB
MPEG-4 movie
Planning 2026-04-15.mp4
15 Apr 2026 at 11:14
2,75 GB
MPEG-4 movie
Retro 2026-04-14.mp4
14 Apr 2026 at 17:37
1,44 GB
MPEG-4 movie
Daily 2026-04-14.mp4
14 Apr 2026 at 10:09
924,4 MB
MPEG-4 movie
User pilot (Adi) 2026-04-09.mp4
9 Apr 2026 at 14:47
362,6 MB
MPEG-4 movie
Daily 2026-04-09.mp4
9 Apr 2026 at 10:07
748,8 MB
MPEG-4 movie
Daily 2026-04-08.mp4
8 Apr 2026 at 10:13
1,04 GB
MPEG-4 movie
Daily 2026-04-07.mp4
7 Apr 2026 at 10:01
575,5 MB
MPEG-4 movie
Daily 2026-04-06.mp4
6 Apr 2026 at 10:08
720,5 MB
MPEG-4 movie
Daily 2026-04-03.mp4
3 Apr 2026 at 10:21
1,02 GB
MPEG-4 movie
Planning 2026-04-01 & task split.mp4
1 Apr 2026 at 12:20
4,68 GB
MPEG-4 movie
Retro 2026-03-31.mp4
31 Mar 2026 at 18:29
3,4 GB
MPEG-4 movie
Daily 2026-03-31.mp4
31 Mar 2026 at 10:10
923,6 MB
MPEG-4 movie
Refinement 2026-03-30.mp4
30 Mar 2026 at 17:12
2,77 GB
MPEG-4 movie
Daily 2026-03-30.mp4
30 Mar 2026 at 10:05
641,8 MB
MPEG-4 movie
Daily 2026-03-27.mp4
27 Mar 2026 at 10:09
884,3 MB
MPEG-4 movie
Daily 2026-03-26.mp4
26 Mar 2026 at 9:59
476,6 MB
MPEG-4 movie
Daily 2026-03-24.mp4
24 Mar 2026 at 10:00
550,8 MB
MPEG-4 movie
Refinement 2026-03-23.mp4
23 Mar 2026 at 17:03
3,44 GB
MPEG-4 movie
Daily 2026-03-23.mp4
23 Mar 2026 at 10:00
438,9 MB
MPEG-4 movie
BE chapter 2026-03-20.mp4
20 Mar 2026 at 11:46
1,68 GB
MPEG-4 movie
Daily 2026-03-20.mp4
20 Mar 2026 at 10:06
430,4 MB
MPEG-4 movie
Planing 2026-03-18-converted.mp4
19 Mar 2026 at 12:01
2,38 GB
MPEG-4 movie
Refinement 2026-02-09-converted.mp4
19 Mar 2026 at 11:35
2,26 GB
MPEG-4 movie
Daily 2026-03-19.mp4
19 Mar 2026 at 9:57
386,3 MB
MPEG-4 movie
Review 2026-03-18.mp4
18 Mar 2026 at 16:20
705,8 MB
MPEG-4 movie
Planing 2026-03-18.mp4
18 Mar 2026 at 11:14
2,78 GB
MPEG-4 movie
Retro 2026-03-17.mp4
17 Mar 2026 at 17:40
1,53 GB
MPEG-4 movie
Daily 2026-03-17.mp4
17 Mar 2026 at 10:18
1,2 GB
MPEG-4 movie
Refinement 2026-03-16.mp4
16 Mar 2026 at 16:55
4,19 GB
MPEG-4 movie
Daily 2026-03-16.mp4
16 Mar 2026 at 10:02
592,2 MB
MPEG-4 movie
Daily 2026-03-13.mp4
13 Mar 2026 at 10:12
1,02 GB
MPEG-4 movie
1-1 2026-03-12.mp4
12 Mar 2026 at 18:35
637,6 MB
MPEG-4 movie
Daily 2026-03-12.mp4
12 Mar 2026 at 10:10
978,7 MB
MPEG-4 movie
Daily 2026-03-11.mp4
11 Mar 2026 at 10:06
798,7 MB
MPEG-4 movie
Daily 2026-03-10.mp4
10 Mar 2026 at 9:57
404,6 MB
MPEG-4 movie
Refinement 2026-03-09.mp4
9 Mar 2026 at 17:04
4,16 GB
MPEG-4 movie
Daily 2026-03-09.mp4
9 Mar 2026 at 9:56
319,7 MB
MPEG-4 movie
Daily 2026-03-06.mp4
6 Mar 2026 at 9:57
291,7 MB
MPEG-4 movie
Planning 2026-03-04.mp4
4 Mar 2026 at 11:09
2,62 GB
MPEG-4 movie
Daily 2026-03-02.mp4
2 Mar 2026 at 10:07
768,5 MB
MPEG-4 movie
Daily 2026-02-27.mp4
27 Feb 2026 at 10:02
546,8 MB
MPEG-4 movie
Daily 2026-02-26.mov
26 Feb 2026 at 9:53
96,6 MB
QT movie
Daily 2026-02-25.mov
25 Feb 2026 at 9:59
503,5 MB
QT movie
Opportunity-Contacts 2026-02-24.mp4
24 Feb 2026 at 12:03
791,7 MB
MPEG-4 movie
Daily 2026-02-24.mp4
24 Feb 2026 at 10:02
520,7 MB
MPEG-4 movie
Refinement 2026-02-23.mov
23 Feb 2026 at 16:31
2 GB
QT movie
Daily 2026-02-20 & Ani.mp4
20 Feb 2026 at 10:53
2,52 GB
MPEG-4 movie
Daily 2026-02-19.mp4
19 Feb 2026 at 9:54
234,2 MB
MPEG-4 movie
Review 2026-02-18.mp4
18 Feb 2026 at 16:25
925,1 MB
MPEG-4 movie
Planning 2026-02-18.mp4
18 Feb 2026 at 10:57
2,04 GB
MPEG-4 movie
Retro 2026-02-17.mp4
17 Feb 2026 at 17:38
1,31 GB
MPEG-4 movie
Refinement & P1 debugging 2026-02-16.mp4
16 Feb 2026 at 17:36
4,53 GB
MPEG-4 movie...
|
NULL
|
|
73423
|
NULL
|
0
|
2026-04-23T07:36:22.957442+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776929782957_m2.jpg...
|
Finder
|
Work
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Favourites
jiminny
AirDrop
Recents
Applications
Do Favourites
jiminny
AirDrop
Recents
Applications
Documents
Downloads
lukas
iCloud
iCloud Drive
Sync folder
Locations
DXP4800PLUS-B5F
Eject
Network
Tags
CRM
Orange
Red
Yellow
Green
Blue
Purple
All Tags…
Name
Date Modified
Size
Kind
2026
Today at 10:32
--
Folder
CleanShot 2026-04-23 at 09.45.36.mp4
Today at 10:32
1,74 GB
MPEG-4 movie
Daily 2026-04-22.mp4
Yesterday at 10:21
1,36 GB
MPEG-4 movie
Refinement 2026-04-06.mp4
21 Apr 2026 at 11:02
2,41 GB
MPEG-4 movie
Daily 2026-04-21.mp4
21 Apr 2026 at 10:00
567,8 MB
MPEG-4 movie
Refinement 2026-04-20.mp4
20 Apr 2026 at 16:56
4,25 GB
MPEG-4 movie
Daily 2026-04-20.mp4
20 Apr 2026 at 10:06
698,5 MB
MPEG-4 movie
Daily 2026-04-17.mp4
17 Apr 2026 at 10:16
1,16 GB
MPEG-4 movie
Daily 2026-04-16.mp4
16 Apr 2026 at 10:00
513,4 MB
MPEG-4 movie
Planning 2026-04-15.mp4
15 Apr 2026 at 11:14
2,75 GB
MPEG-4 movie
Retro 2026-04-14.mp4
14 Apr 2026 at 17:37
1,44 GB
MPEG-4 movie
Daily 2026-04-14.mp4
14 Apr 2026 at 10:09
924,4 MB
MPEG-4 movie
User pilot (Adi) 2026-04-09.mp4
9 Apr 2026 at 14:47
362,6 MB
MPEG-4 movie
Daily 2026-04-09.mp4
9 Apr 2026 at 10:07
748,8 MB
MPEG-4 movie
Daily 2026-04-08.mp4
8 Apr 2026 at 10:13
1,04 GB
MPEG-4 movie
Daily 2026-04-07.mp4
7 Apr 2026 at 10:01
575,5 MB
MPEG-4 movie
Daily 2026-04-06.mp4
6 Apr 2026 at 10:08
720,5 MB
MPEG-4 movie
Daily 2026-04-03.mp4
3 Apr 2026 at 10:21
1,02 GB
MPEG-4 movie
Planning 2026-04-01 & task split.mp4
1 Apr 2026 at 12:20
4,68 GB
MPEG-4 movie
Retro 2026-03-31.mp4
31 Mar 2026 at 18:29
3,4 GB
MPEG-4 movie
Daily 2026-03-31.mp4
31 Mar 2026 at 10:10
923,6 MB
MPEG-4 movie
Refinement 2026-03-30.mp4
30 Mar 2026 at 17:12
2,77 GB
MPEG-4 movie
Daily 2026-03-30.mp4
30 Mar 2026 at 10:05
641,8 MB
MPEG-4 movie
Daily 2026-03-27.mp4
27 Mar 2026 at 10:09
884,3 MB
MPEG-4 movie
Daily 2026-03-26.mp4
26 Mar 2026 at 9:59
476,6 MB
MPEG-4 movie
Daily 2026-03-24.mp4
24 Mar 2026 at 10:00
550,8 MB
MPEG-4 movie
Refinement 2026-03-23.mp4
23 Mar 2026 at 17:03
3,44 GB
MPEG-4 movie
Daily 2026-03-23.mp4
23 Mar 2026 at 10:00
438,9 MB
MPEG-4 movie
BE chapter 2026-03-20.mp4
20 Mar 2026 at 11:46
1,68 GB
MPEG-4 movie
Daily 2026-03-20.mp4
20 Mar 2026 at 10:06
430,4 MB
MPEG-4 movie
Planing 2026-03-18-converted.mp4
19 Mar 2026 at 12:01
2,38 GB
MPEG-4 movie
Refinement 2026-02-09-converted.mp4
19 Mar 2026 at 11:35
2,26 GB
MPEG-4 movie
Daily 2026-03-19.mp4
19 Mar 2026 at 9:57
386,3 MB
MPEG-4 movie
Review 2026-03-18.mp4
18 Mar 2026 at 16:20
705,8 MB
MPEG-4 movie
Planing 2026-03-18.mp4
18 Mar 2026 at 11:14
2,78 GB
MPEG-4 movie
Retro 2026-03-17.mp4
17 Mar 2026 at 17:40
1,53 GB
MPEG-4 movie
Daily 2026-03-17.mp4
17 Mar 2026 at 10:18
1,2 GB
MPEG-4 movie
Refinement 2026-03-16.mp4
16 Mar 2026 at 16:55
4,19 GB
MPEG-4 movie
Daily 2026-03-16.mp4
16 Mar 2026 at 10:02
592,2 MB
MPEG-4 movie
Daily 2026-03-13.mp4
13 Mar 2026 at 10:12
1,02 GB
MPEG-4 movie
1-1 2026-03-12.mp4
12 Mar 2026 at 18:35
637,6 MB
MPEG-4 movie
Daily 2026-03-12.mp4
12 Mar 2026 at 10:10
978,7 MB
MPEG-4 movie
Daily 2026-03-11.mp4
11 Mar 2026 at 10:06
798,7 MB
MPEG-4 movie
Daily 2026-03-10.mp4
10 Mar 2026 at 9:57
404,6 MB
MPEG-4 movie
Refinement 2026-03-09.mp4
9 Mar 2026 at 17:04
4,16 GB
MPEG-4 movie
Daily 2026-03-09.mp4
9 Mar 2026 at 9:56
319,7 MB
MPEG-4 movie
Daily 2026-03-06.mp4
6 Mar 2026 at 9:57
291,7 MB
MPEG-4 movie
Planning 2026-03-04.mp4
4 Mar 2026 at 11:09
2,62 GB
MPEG-4 movie
Daily 2026-03-02.mp4
2 Mar 2026 at 10:07
768,5 MB
MPEG-4 movie
Daily 2026-02-27.mp4
27 Feb 2026 at 10:02
546,8 MB
MPEG-4 movie
Daily 2026-02-26.mov
26 Feb 2026 at 9:53
96,6 MB
QT movie
Daily 2026-02-25.mov
25 Feb 2026 at 9:59
503,5 MB
QT movie
Opportunity-Contacts 2026-02-24.mp4
24 Feb 2026 at 12:03
791,7 MB
MPEG-4 movie
Daily 2026-02-24.mp4
24 Feb 2026 at 10:02
520,7 MB
MPEG-4 movie
Refinement 2026-02-23.mov
23 Feb 2026 at 16:31
2 GB
QT movie
Daily 2026-02-20 & Ani.mp4
20 Feb 2026 at 10:53
2,52 GB
MPEG-4 movie
Daily 2026-02-19.mp4
19 Feb 2026 at 9:54
234,2 MB
MPEG-4 movie
Review 2026-02-18.mp4
18 Feb 2026 at 16:25
925,1 MB
MPEG-4 movie
Planning 2026-02-18.mp4
18 Feb 2026 at 10:57
2,04 GB
MPEG-4 movie
Retro 2026-02-17.mp4
17 Feb 2026 at 17:38
1,31 GB
MPEG-4 movie
Refinement & P1 debugging 2026-02-16.mp4...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Favourites","depth":6,"bounds":{"left":0.5046542,"top":0.061452515,"width":0.06216755,"height":0.015163607},"automation_id":"xSidebarHeader","role_description":"text"},{"role":"AXStaticText","text":"jiminny","depth":6,"bounds":{"left":0.51263297,"top":0.08140463,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"AirDrop","depth":6,"bounds":{"left":0.51263297,"top":0.103751,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"Recents","depth":6,"bounds":{"left":0.51263297,"top":0.12609737,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"Applications","depth":6,"bounds":{"left":0.51263297,"top":0.14844373,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"Documents","depth":6,"bounds":{"left":0.51263297,"top":0.1707901,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"Downloads","depth":6,"bounds":{"left":0.51263297,"top":0.19313647,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"lukas","depth":6,"bounds":{"left":0.51263297,"top":0.21548285,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"iCloud","depth":6,"bounds":{"left":0.5046542,"top":0.2434158,"width":0.06216755,"height":0.015163607},"automation_id":"xSidebarHeader","role_description":"text"},{"role":"AXStaticText","text":"iCloud Drive","depth":6,"bounds":{"left":0.51263297,"top":0.26336792,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"Sync folder","depth":6,"bounds":{"left":0.51263297,"top":0.2857143,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"Locations","depth":6,"bounds":{"left":0.5046542,"top":0.31364724,"width":0.06216755,"height":0.015163607},"automation_id":"xSidebarHeader","role_description":"text"},{"role":"AXStaticText","text":"DXP4800PLUS-B5F","depth":6,"bounds":{"left":0.51263297,"top":0.33359936,"width":0.043218084,"height":0.012769354},"role_description":"text"},{"role":"AXButton","text":"Eject","depth":6,"bounds":{"left":0.55651593,"top":0.33519554,"width":0.0043218085,"height":0.009577015},"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"Network","depth":6,"bounds":{"left":0.51263297,"top":0.35594574,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"Tags","depth":6,"bounds":{"left":0.5046542,"top":0.38387868,"width":0.06216755,"height":0.015163607},"automation_id":"xSidebarHeader","role_description":"text"},{"role":"AXStaticText","text":"CRM","depth":6,"bounds":{"left":0.51263297,"top":0.4038308,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"Orange","depth":6,"bounds":{"left":0.51263297,"top":0.42617717,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"Red","depth":6,"bounds":{"left":0.51263297,"top":0.44852355,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"Yellow","depth":6,"bounds":{"left":0.51263297,"top":0.4708699,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"Green","depth":6,"bounds":{"left":0.51263297,"top":0.49321628,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"Blue","depth":6,"bounds":{"left":0.51263297,"top":0.51556265,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"Purple","depth":6,"bounds":{"left":0.51263297,"top":0.53790903,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"All Tags…","depth":6,"bounds":{"left":0.51263297,"top":0.5602554,"width":0.049534574,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"Name","depth":7,"bounds":{"left":0.5827792,"top":0.06624102,"width":0.011635638,"height":0.011173184},"role_description":"text"},{"role":"AXStaticText","text":"Date Modified","depth":7,"bounds":{"left":0.8656915,"top":0.06624102,"width":0.026928192,"height":0.011173184},"role_description":"text"},{"role":"AXStaticText","text":"Size","depth":7,"bounds":{"left":0.92586434,"top":0.06624102,"width":0.008976064,"height":0.011173184},"role_description":"text"},{"role":"AXStaticText","text":"Kind","depth":7,"bounds":{"left":0.9581117,"top":0.06624102,"width":0.00930851,"height":0.011173184},"role_description":"text"},{"role":"AXTextField","text":"2026","depth":7,"bounds":{"left":0.5827792,"top":0.08938547,"width":0.013297873,"height":0.012769354},"value":"2026","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Today at 10:32","depth":7,"bounds":{"left":0.8656915,"top":0.08938547,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"--","depth":7,"bounds":{"left":0.9494681,"top":0.08938547,"width":0.0056515955,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"Folder","depth":7,"bounds":{"left":0.9581117,"top":0.08938547,"width":0.014295213,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"CleanShot 2026-04-23 at 09.45.36.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.105347164,"width":0.084773935,"height":0.012769354},"value":"CleanShot 2026-04-23 at 09.45.36.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Today at 10:32","depth":7,"bounds":{"left":0.8656915,"top":0.105347164,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"1,74 GB","depth":7,"bounds":{"left":0.9368351,"top":0.105347164,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.105347164,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-22.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.121308856,"width":0.049534574,"height":0.012769354},"value":"Daily 2026-04-22.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Yesterday at 10:21","depth":7,"bounds":{"left":0.8656915,"top":0.121308856,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"1,36 GB","depth":7,"bounds":{"left":0.9368351,"top":0.121308856,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.121308856,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-04-06.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.13727055,"width":0.0625,"height":0.012769354},"value":"Refinement 2026-04-06.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"21 Apr 2026 at 11:02","depth":7,"bounds":{"left":0.8656915,"top":0.13727055,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"2,41 GB","depth":7,"bounds":{"left":0.9368351,"top":0.13727055,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.13727055,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-21.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.15323225,"width":0.04886968,"height":0.012769354},"value":"Daily 2026-04-21.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"21 Apr 2026 at 10:00","depth":7,"bounds":{"left":0.8656915,"top":0.15323225,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"567,8 MB","depth":7,"bounds":{"left":0.93351066,"top":0.15323225,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.15323225,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-04-20.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.16919394,"width":0.0625,"height":0.012769354},"value":"Refinement 2026-04-20.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"20 Apr 2026 at 16:56","depth":7,"bounds":{"left":0.8656915,"top":0.16919394,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"4,25 GB","depth":7,"bounds":{"left":0.9368351,"top":0.16919394,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.16919394,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-20.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.18515563,"width":0.049534574,"height":0.012769354},"value":"Daily 2026-04-20.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"20 Apr 2026 at 10:06","depth":7,"bounds":{"left":0.8656915,"top":0.18515563,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"698,5 MB","depth":7,"bounds":{"left":0.93351066,"top":0.18515563,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.18515563,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-17.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.20111732,"width":0.048204787,"height":0.012769354},"value":"Daily 2026-04-17.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"17 Apr 2026 at 10:16","depth":7,"bounds":{"left":0.8656915,"top":0.20111732,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"1,16 GB","depth":7,"bounds":{"left":0.9368351,"top":0.20111732,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.20111732,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-16.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.21707901,"width":0.04886968,"height":0.012769354},"value":"Daily 2026-04-16.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"16 Apr 2026 at 10:00","depth":7,"bounds":{"left":0.8656915,"top":0.21707901,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"513,4 MB","depth":7,"bounds":{"left":0.93351066,"top":0.21707901,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.21707901,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Planning 2026-04-15.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.2330407,"width":0.05618351,"height":0.012769354},"value":"Planning 2026-04-15.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"15 Apr 2026 at 11:14","depth":7,"bounds":{"left":0.8656915,"top":0.2330407,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"2,75 GB","depth":7,"bounds":{"left":0.9368351,"top":0.2330407,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.2330407,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Retro 2026-04-14.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.2490024,"width":0.049867023,"height":0.012769354},"value":"Retro 2026-04-14.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"14 Apr 2026 at 17:37","depth":7,"bounds":{"left":0.8656915,"top":0.2490024,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"1,44 GB","depth":7,"bounds":{"left":0.9368351,"top":0.2490024,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.2490024,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-14.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.26496407,"width":0.04886968,"height":0.012769354},"value":"Daily 2026-04-14.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"14 Apr 2026 at 10:09","depth":7,"bounds":{"left":0.8656915,"top":0.26496407,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"924,4 MB","depth":7,"bounds":{"left":0.93351066,"top":0.26496407,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.26496407,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"User pilot (Adi) 2026-04-09.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.28092578,"width":0.07014628,"height":0.012769354},"value":"User pilot (Adi) 2026-04-09.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"9 Apr 2026 at 14:47","depth":7,"bounds":{"left":0.8656915,"top":0.28092578,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"362,6 MB","depth":7,"bounds":{"left":0.93351066,"top":0.28092578,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.28092578,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-09.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.29688746,"width":0.049534574,"height":0.012769354},"value":"Daily 2026-04-09.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"9 Apr 2026 at 10:07","depth":7,"bounds":{"left":0.8656915,"top":0.29688746,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"748,8 MB","depth":7,"bounds":{"left":0.93351066,"top":0.29688746,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.29688746,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-08.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.31284916,"width":0.049534574,"height":0.012769354},"value":"Daily 2026-04-08.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"8 Apr 2026 at 10:13","depth":7,"bounds":{"left":0.8656915,"top":0.31284916,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"1,04 GB","depth":7,"bounds":{"left":0.9368351,"top":0.31284916,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.31284916,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-07.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.32881084,"width":0.04886968,"height":0.012769354},"value":"Daily 2026-04-07.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"7 Apr 2026 at 10:01","depth":7,"bounds":{"left":0.8656915,"top":0.32881084,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"575,5 MB","depth":7,"bounds":{"left":0.93351066,"top":0.32881084,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.32881084,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-06.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.34477255,"width":0.049534574,"height":0.012769354},"value":"Daily 2026-04-06.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"6 Apr 2026 at 10:08","depth":7,"bounds":{"left":0.8656915,"top":0.34477255,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"720,5 MB","depth":7,"bounds":{"left":0.93351066,"top":0.34477255,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.34477255,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-03.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.36073422,"width":0.049534574,"height":0.012769354},"value":"Daily 2026-04-03.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"3 Apr 2026 at 10:21","depth":7,"bounds":{"left":0.8656915,"top":0.36073422,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"1,02 GB","depth":7,"bounds":{"left":0.9368351,"top":0.36073422,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.36073422,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Planning 2026-04-01 & task split.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.37669593,"width":0.080119684,"height":0.012769354},"value":"Planning 2026-04-01 & task split.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"1 Apr 2026 at 12:20","depth":7,"bounds":{"left":0.8656915,"top":0.37669593,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"4,68 GB","depth":7,"bounds":{"left":0.9368351,"top":0.37669593,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.37669593,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Retro 2026-03-31.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.3926576,"width":0.049867023,"height":0.012769354},"value":"Retro 2026-03-31.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"31 Mar 2026 at 18:29","depth":7,"bounds":{"left":0.8656915,"top":0.3926576,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"3,4 GB","depth":7,"bounds":{"left":0.93949467,"top":0.3926576,"width":0.015292553,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.3926576,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-31.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.4086193,"width":0.04886968,"height":0.012769354},"value":"Daily 2026-03-31.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"31 Mar 2026 at 10:10","depth":7,"bounds":{"left":0.8656915,"top":0.4086193,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"923,6 MB","depth":7,"bounds":{"left":0.93351066,"top":0.4086193,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.4086193,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-03-30.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.424581,"width":0.0625,"height":0.012769354},"value":"Refinement 2026-03-30.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"30 Mar 2026 at 17:12","depth":7,"bounds":{"left":0.8656915,"top":0.424581,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"2,77 GB","depth":7,"bounds":{"left":0.9368351,"top":0.424581,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.424581,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-30.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.4405427,"width":0.049534574,"height":0.012769354},"value":"Daily 2026-03-30.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"30 Mar 2026 at 10:05","depth":7,"bounds":{"left":0.8656915,"top":0.4405427,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"641,8 MB","depth":7,"bounds":{"left":0.93351066,"top":0.4405427,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.4405427,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-27.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.45650437,"width":0.04886968,"height":0.012769354},"value":"Daily 2026-03-27.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"27 Mar 2026 at 10:09","depth":7,"bounds":{"left":0.8656915,"top":0.45650437,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"884,3 MB","depth":7,"bounds":{"left":0.93351066,"top":0.45650437,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.45650437,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-26.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.47246608,"width":0.049534574,"height":0.012769354},"value":"Daily 2026-03-26.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"26 Mar 2026 at 9:59","depth":7,"bounds":{"left":0.8656915,"top":0.47246608,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"476,6 MB","depth":7,"bounds":{"left":0.93351066,"top":0.47246608,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.47246608,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-24.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.4884278,"width":0.049534574,"height":0.012769354},"value":"Daily 2026-03-24.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"24 Mar 2026 at 10:00","depth":7,"bounds":{"left":0.8656915,"top":0.4884278,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"550,8 MB","depth":7,"bounds":{"left":0.93351066,"top":0.4884278,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.4884278,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-03-23.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.50438946,"width":0.0625,"height":0.012769354},"value":"Refinement 2026-03-23.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"23 Mar 2026 at 17:03","depth":7,"bounds":{"left":0.8656915,"top":0.50438946,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"3,44 GB","depth":7,"bounds":{"left":0.9368351,"top":0.50438946,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.50438946,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-23.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.5203512,"width":0.049534574,"height":0.012769354},"value":"Daily 2026-03-23.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"23 Mar 2026 at 10:00","depth":7,"bounds":{"left":0.8656915,"top":0.5203512,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"438,9 MB","depth":7,"bounds":{"left":0.93351066,"top":0.5203512,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.5203512,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"BE chapter 2026-03-20.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.5363129,"width":0.061502658,"height":0.012769354},"value":"BE chapter 2026-03-20.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"20 Mar 2026 at 11:46","depth":7,"bounds":{"left":0.8656915,"top":0.5363129,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"1,68 GB","depth":7,"bounds":{"left":0.9368351,"top":0.5363129,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.5363129,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-20.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.5522745,"width":0.049534574,"height":0.012769354},"value":"Daily 2026-03-20.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"20 Mar 2026 at 10:06","depth":7,"bounds":{"left":0.8656915,"top":0.5522745,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"430,4 MB","depth":7,"bounds":{"left":0.93351066,"top":0.5522745,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.5522745,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Planing 2026-03-18-converted.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.56823623,"width":0.076130316,"height":0.012769354},"value":"Planing 2026-03-18-converted.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"19 Mar 2026 at 12:01","depth":7,"bounds":{"left":0.8656915,"top":0.56823623,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"2,38 GB","depth":7,"bounds":{"left":0.9368351,"top":0.56823623,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.56823623,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-02-09-converted.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.58419794,"width":0.084773935,"height":0.012769354},"value":"Refinement 2026-02-09-converted.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"19 Mar 2026 at 11:35","depth":7,"bounds":{"left":0.8656915,"top":0.58419794,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"2,26 GB","depth":7,"bounds":{"left":0.9368351,"top":0.58419794,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.58419794,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-19.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.60015965,"width":0.04886968,"height":0.012769354},"value":"Daily 2026-03-19.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"19 Mar 2026 at 9:57","depth":7,"bounds":{"left":0.8656915,"top":0.60015965,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"386,3 MB","depth":7,"bounds":{"left":0.93351066,"top":0.60015965,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.60015965,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Review 2026-03-18.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.6161213,"width":0.05319149,"height":0.012769354},"value":"Review 2026-03-18.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"18 Mar 2026 at 16:20","depth":7,"bounds":{"left":0.8656915,"top":0.6161213,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"705,8 MB","depth":7,"bounds":{"left":0.93351066,"top":0.6161213,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.6161213,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Planing 2026-03-18.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.632083,"width":0.053856384,"height":0.012769354},"value":"Planing 2026-03-18.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"18 Mar 2026 at 11:14","depth":7,"bounds":{"left":0.8656915,"top":0.632083,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"2,78 GB","depth":7,"bounds":{"left":0.9368351,"top":0.632083,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.632083,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Retro 2026-03-17.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.6480447,"width":0.04920213,"height":0.012769354},"value":"Retro 2026-03-17.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"17 Mar 2026 at 17:40","depth":7,"bounds":{"left":0.8656915,"top":0.6480447,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"1,53 GB","depth":7,"bounds":{"left":0.9368351,"top":0.6480447,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.6480447,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-17.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.6640064,"width":0.048204787,"height":0.012769354},"value":"Daily 2026-03-17.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"17 Mar 2026 at 10:18","depth":7,"bounds":{"left":0.8656915,"top":0.6640064,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"1,2 GB","depth":7,"bounds":{"left":0.93949467,"top":0.6640064,"width":0.015292553,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.6640064,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-03-16.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.67996806,"width":0.061835106,"height":0.012769354},"value":"Refinement 2026-03-16.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"16 Mar 2026 at 16:55","depth":7,"bounds":{"left":0.8656915,"top":0.67996806,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"4,19 GB","depth":7,"bounds":{"left":0.9368351,"top":0.67996806,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.67996806,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-16.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.69592977,"width":0.04886968,"height":0.012769354},"value":"Daily 2026-03-16.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"16 Mar 2026 at 10:02","depth":7,"bounds":{"left":0.8656915,"top":0.69592977,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"592,2 MB","depth":7,"bounds":{"left":0.93351066,"top":0.69592977,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.69592977,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-13.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.7118915,"width":0.04886968,"height":0.012769354},"value":"Daily 2026-03-13.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"13 Mar 2026 at 10:12","depth":7,"bounds":{"left":0.8656915,"top":0.7118915,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"1,02 GB","depth":7,"bounds":{"left":0.9368351,"top":0.7118915,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.7118915,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"1-1 2026-03-12.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.7278532,"width":0.04488032,"height":0.012769354},"value":"1-1 2026-03-12.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"12 Mar 2026 at 18:35","depth":7,"bounds":{"left":0.8656915,"top":0.7278532,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"637,6 MB","depth":7,"bounds":{"left":0.93351066,"top":0.7278532,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.7278532,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-12.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.7438148,"width":0.04886968,"height":0.012769354},"value":"Daily 2026-03-12.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"12 Mar 2026 at 10:10","depth":7,"bounds":{"left":0.8656915,"top":0.7438148,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"978,7 MB","depth":7,"bounds":{"left":0.93351066,"top":0.7438148,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.7438148,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-11.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.75977653,"width":0.048204787,"height":0.012769354},"value":"Daily 2026-03-11.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"11 Mar 2026 at 10:06","depth":7,"bounds":{"left":0.8656915,"top":0.75977653,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"798,7 MB","depth":7,"bounds":{"left":0.93351066,"top":0.75977653,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.75977653,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-10.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.77573824,"width":0.04886968,"height":0.012769354},"value":"Daily 2026-03-10.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"10 Mar 2026 at 9:57","depth":7,"bounds":{"left":0.8656915,"top":0.77573824,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"404,6 MB","depth":7,"bounds":{"left":0.93351066,"top":0.77573824,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.77573824,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-03-09.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.79169995,"width":0.0625,"height":0.012769354},"value":"Refinement 2026-03-09.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"9 Mar 2026 at 17:04","depth":7,"bounds":{"left":0.8656915,"top":0.79169995,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"4,16 GB","depth":7,"bounds":{"left":0.9368351,"top":0.79169995,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.79169995,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-09.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.8076616,"width":0.049534574,"height":0.012769354},"value":"Daily 2026-03-09.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"9 Mar 2026 at 9:56","depth":7,"bounds":{"left":0.8656915,"top":0.8076616,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"319,7 MB","depth":7,"bounds":{"left":0.93351066,"top":0.8076616,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.8076616,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-06.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.8236233,"width":0.049534574,"height":0.012769354},"value":"Daily 2026-03-06.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"6 Mar 2026 at 9:57","depth":7,"bounds":{"left":0.8656915,"top":0.8236233,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"291,7 MB","depth":7,"bounds":{"left":0.93351066,"top":0.8236233,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.8236233,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Planning 2026-03-04.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.839585,"width":0.056848403,"height":0.012769354},"value":"Planning 2026-03-04.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 Mar 2026 at 11:09","depth":7,"bounds":{"left":0.8656915,"top":0.839585,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"2,62 GB","depth":7,"bounds":{"left":0.9368351,"top":0.839585,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.839585,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-02.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.8555467,"width":0.049534574,"height":0.012769354},"value":"Daily 2026-03-02.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"2 Mar 2026 at 10:07","depth":7,"bounds":{"left":0.8656915,"top":0.8555467,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"768,5 MB","depth":7,"bounds":{"left":0.93351066,"top":0.8555467,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.8555467,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-02-27.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.87150836,"width":0.04886968,"height":0.012769354},"value":"Daily 2026-02-27.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"27 Feb 2026 at 10:02","depth":7,"bounds":{"left":0.8656915,"top":0.87150836,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"546,8 MB","depth":7,"bounds":{"left":0.93351066,"top":0.87150836,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.87150836,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-02-26.mov","depth":7,"bounds":{"left":0.58776593,"top":0.88747007,"width":0.04886968,"height":0.012769354},"value":"Daily 2026-02-26.mov","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"26 Feb 2026 at 9:53","depth":7,"bounds":{"left":0.8656915,"top":0.88747007,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"96,6 MB","depth":7,"bounds":{"left":0.93650264,"top":0.88747007,"width":0.01861702,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"QT movie","depth":7,"bounds":{"left":0.9581117,"top":0.88747007,"width":0.034906916,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-02-25.mov","depth":7,"bounds":{"left":0.58776593,"top":0.9034318,"width":0.048537236,"height":0.012769354},"value":"Daily 2026-02-25.mov","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"25 Feb 2026 at 9:59","depth":7,"bounds":{"left":0.8656915,"top":0.9034318,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"503,5 MB","depth":7,"bounds":{"left":0.93351066,"top":0.9034318,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"QT movie","depth":7,"bounds":{"left":0.9581117,"top":0.9034318,"width":0.034906916,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Opportunity-Contacts 2026-02-24.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.9193935,"width":0.08377659,"height":0.012769354},"value":"Opportunity-Contacts 2026-02-24.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"24 Feb 2026 at 12:03","depth":7,"bounds":{"left":0.8656915,"top":0.9193935,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"791,7 MB","depth":7,"bounds":{"left":0.93351066,"top":0.9193935,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.9193935,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-02-24.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.9353551,"width":0.04920213,"height":0.012769354},"value":"Daily 2026-02-24.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"24 Feb 2026 at 10:02","depth":7,"bounds":{"left":0.8656915,"top":0.9353551,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"520,7 MB","depth":7,"bounds":{"left":0.93351066,"top":0.9353551,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.9353551,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-02-23.mov","depth":7,"bounds":{"left":0.58776593,"top":0.95131683,"width":0.061835106,"height":0.012769354},"value":"Refinement 2026-02-23.mov","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"23 Feb 2026 at 16:31","depth":7,"bounds":{"left":0.8656915,"top":0.95131683,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"2 GB","depth":7,"bounds":{"left":0.94348407,"top":0.95131683,"width":0.011303191,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"QT movie","depth":7,"bounds":{"left":0.9581117,"top":0.95131683,"width":0.034906916,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-02-20 & Ani.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.96727854,"width":0.061170213,"height":0.012769354},"value":"Daily 2026-02-20 & Ani.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"20 Feb 2026 at 10:53","depth":7,"bounds":{"left":0.8656915,"top":0.96727854,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"2,52 GB","depth":7,"bounds":{"left":0.9368351,"top":0.96727854,"width":0.017952127,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.96727854,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-02-19.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.98324025,"width":0.04886968,"height":0.012769354},"value":"Daily 2026-02-19.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"19 Feb 2026 at 9:54","depth":7,"bounds":{"left":0.8656915,"top":0.98324025,"width":0.056848403,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"234,2 MB","depth":7,"bounds":{"left":0.93351066,"top":0.98324025,"width":0.021276595,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.98324025,"width":0.032912236,"height":0.012769354},"role_description":"text"},{"role":"AXTextField","text":"Review 2026-02-18.mp4","depth":7,"bounds":{"left":0.58776593,"top":0.9992019,"width":0.05319149,"height":0.0007981062},"value":"Review 2026-02-18.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"18 Feb 2026 at 16:25","depth":7,"bounds":{"left":0.8656915,"top":0.9992019,"width":0.056848403,"height":0.0007981062},"role_description":"text"},{"role":"AXStaticText","text":"925,1 MB","depth":7,"bounds":{"left":0.93351066,"top":0.9992019,"width":0.021276595,"height":0.0007981062},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":0.9992019,"width":0.032912236,"height":0.0007981062},"role_description":"text"},{"role":"AXTextField","text":"Planning 2026-02-18.mp4","depth":7,"bounds":{"left":0.58776593,"top":1.0,"width":0.05618351,"height":-0.01516366},"value":"Planning 2026-02-18.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"18 Feb 2026 at 10:57","depth":7,"bounds":{"left":0.8656915,"top":1.0,"width":0.056848403,"height":-0.01516366},"role_description":"text"},{"role":"AXStaticText","text":"2,04 GB","depth":7,"bounds":{"left":0.9368351,"top":1.0,"width":0.017952127,"height":-0.01516366},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":1.0,"width":0.032912236,"height":-0.01516366},"role_description":"text"},{"role":"AXTextField","text":"Retro 2026-02-17.mp4","depth":7,"bounds":{"left":0.58776593,"top":1.0,"width":0.05618351,"height":-0.031125307},"value":"Retro 2026-02-17.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"17 Feb 2026 at 17:38","depth":7,"bounds":{"left":0.8656915,"top":1.0,"width":0.056848403,"height":-0.031125307},"role_description":"text"},{"role":"AXStaticText","text":"1,31 GB","depth":7,"bounds":{"left":0.9368351,"top":1.0,"width":0.017952127,"height":-0.031125307},"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"bounds":{"left":0.9581117,"top":1.0,"width":0.032912236,"height":-0.031125307},"role_description":"text"},{"role":"AXTextField","text":"Refinement & P1 debugging 2026-02-16.mp4","depth":7,"bounds":{"left":0.58776593,"top":1.0,"width":0.04886968,"height":-0.047086954},"value":"Refinement & P1 debugging 2026-02-16.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
-5626541066854626210
|
-2910781466942463541
|
idle
|
accessibility
|
NULL
|
Favourites
jiminny
AirDrop
Recents
Applications
Do Favourites
jiminny
AirDrop
Recents
Applications
Documents
Downloads
lukas
iCloud
iCloud Drive
Sync folder
Locations
DXP4800PLUS-B5F
Eject
Network
Tags
CRM
Orange
Red
Yellow
Green
Blue
Purple
All Tags…
Name
Date Modified
Size
Kind
2026
Today at 10:32
--
Folder
CleanShot 2026-04-23 at 09.45.36.mp4
Today at 10:32
1,74 GB
MPEG-4 movie
Daily 2026-04-22.mp4
Yesterday at 10:21
1,36 GB
MPEG-4 movie
Refinement 2026-04-06.mp4
21 Apr 2026 at 11:02
2,41 GB
MPEG-4 movie
Daily 2026-04-21.mp4
21 Apr 2026 at 10:00
567,8 MB
MPEG-4 movie
Refinement 2026-04-20.mp4
20 Apr 2026 at 16:56
4,25 GB
MPEG-4 movie
Daily 2026-04-20.mp4
20 Apr 2026 at 10:06
698,5 MB
MPEG-4 movie
Daily 2026-04-17.mp4
17 Apr 2026 at 10:16
1,16 GB
MPEG-4 movie
Daily 2026-04-16.mp4
16 Apr 2026 at 10:00
513,4 MB
MPEG-4 movie
Planning 2026-04-15.mp4
15 Apr 2026 at 11:14
2,75 GB
MPEG-4 movie
Retro 2026-04-14.mp4
14 Apr 2026 at 17:37
1,44 GB
MPEG-4 movie
Daily 2026-04-14.mp4
14 Apr 2026 at 10:09
924,4 MB
MPEG-4 movie
User pilot (Adi) 2026-04-09.mp4
9 Apr 2026 at 14:47
362,6 MB
MPEG-4 movie
Daily 2026-04-09.mp4
9 Apr 2026 at 10:07
748,8 MB
MPEG-4 movie
Daily 2026-04-08.mp4
8 Apr 2026 at 10:13
1,04 GB
MPEG-4 movie
Daily 2026-04-07.mp4
7 Apr 2026 at 10:01
575,5 MB
MPEG-4 movie
Daily 2026-04-06.mp4
6 Apr 2026 at 10:08
720,5 MB
MPEG-4 movie
Daily 2026-04-03.mp4
3 Apr 2026 at 10:21
1,02 GB
MPEG-4 movie
Planning 2026-04-01 & task split.mp4
1 Apr 2026 at 12:20
4,68 GB
MPEG-4 movie
Retro 2026-03-31.mp4
31 Mar 2026 at 18:29
3,4 GB
MPEG-4 movie
Daily 2026-03-31.mp4
31 Mar 2026 at 10:10
923,6 MB
MPEG-4 movie
Refinement 2026-03-30.mp4
30 Mar 2026 at 17:12
2,77 GB
MPEG-4 movie
Daily 2026-03-30.mp4
30 Mar 2026 at 10:05
641,8 MB
MPEG-4 movie
Daily 2026-03-27.mp4
27 Mar 2026 at 10:09
884,3 MB
MPEG-4 movie
Daily 2026-03-26.mp4
26 Mar 2026 at 9:59
476,6 MB
MPEG-4 movie
Daily 2026-03-24.mp4
24 Mar 2026 at 10:00
550,8 MB
MPEG-4 movie
Refinement 2026-03-23.mp4
23 Mar 2026 at 17:03
3,44 GB
MPEG-4 movie
Daily 2026-03-23.mp4
23 Mar 2026 at 10:00
438,9 MB
MPEG-4 movie
BE chapter 2026-03-20.mp4
20 Mar 2026 at 11:46
1,68 GB
MPEG-4 movie
Daily 2026-03-20.mp4
20 Mar 2026 at 10:06
430,4 MB
MPEG-4 movie
Planing 2026-03-18-converted.mp4
19 Mar 2026 at 12:01
2,38 GB
MPEG-4 movie
Refinement 2026-02-09-converted.mp4
19 Mar 2026 at 11:35
2,26 GB
MPEG-4 movie
Daily 2026-03-19.mp4
19 Mar 2026 at 9:57
386,3 MB
MPEG-4 movie
Review 2026-03-18.mp4
18 Mar 2026 at 16:20
705,8 MB
MPEG-4 movie
Planing 2026-03-18.mp4
18 Mar 2026 at 11:14
2,78 GB
MPEG-4 movie
Retro 2026-03-17.mp4
17 Mar 2026 at 17:40
1,53 GB
MPEG-4 movie
Daily 2026-03-17.mp4
17 Mar 2026 at 10:18
1,2 GB
MPEG-4 movie
Refinement 2026-03-16.mp4
16 Mar 2026 at 16:55
4,19 GB
MPEG-4 movie
Daily 2026-03-16.mp4
16 Mar 2026 at 10:02
592,2 MB
MPEG-4 movie
Daily 2026-03-13.mp4
13 Mar 2026 at 10:12
1,02 GB
MPEG-4 movie
1-1 2026-03-12.mp4
12 Mar 2026 at 18:35
637,6 MB
MPEG-4 movie
Daily 2026-03-12.mp4
12 Mar 2026 at 10:10
978,7 MB
MPEG-4 movie
Daily 2026-03-11.mp4
11 Mar 2026 at 10:06
798,7 MB
MPEG-4 movie
Daily 2026-03-10.mp4
10 Mar 2026 at 9:57
404,6 MB
MPEG-4 movie
Refinement 2026-03-09.mp4
9 Mar 2026 at 17:04
4,16 GB
MPEG-4 movie
Daily 2026-03-09.mp4
9 Mar 2026 at 9:56
319,7 MB
MPEG-4 movie
Daily 2026-03-06.mp4
6 Mar 2026 at 9:57
291,7 MB
MPEG-4 movie
Planning 2026-03-04.mp4
4 Mar 2026 at 11:09
2,62 GB
MPEG-4 movie
Daily 2026-03-02.mp4
2 Mar 2026 at 10:07
768,5 MB
MPEG-4 movie
Daily 2026-02-27.mp4
27 Feb 2026 at 10:02
546,8 MB
MPEG-4 movie
Daily 2026-02-26.mov
26 Feb 2026 at 9:53
96,6 MB
QT movie
Daily 2026-02-25.mov
25 Feb 2026 at 9:59
503,5 MB
QT movie
Opportunity-Contacts 2026-02-24.mp4
24 Feb 2026 at 12:03
791,7 MB
MPEG-4 movie
Daily 2026-02-24.mp4
24 Feb 2026 at 10:02
520,7 MB
MPEG-4 movie
Refinement 2026-02-23.mov
23 Feb 2026 at 16:31
2 GB
QT movie
Daily 2026-02-20 & Ani.mp4
20 Feb 2026 at 10:53
2,52 GB
MPEG-4 movie
Daily 2026-02-19.mp4
19 Feb 2026 at 9:54
234,2 MB
MPEG-4 movie
Review 2026-02-18.mp4
18 Feb 2026 at 16:25
925,1 MB
MPEG-4 movie
Planning 2026-02-18.mp4
18 Feb 2026 at 10:57
2,04 GB
MPEG-4 movie
Retro 2026-02-17.mp4
17 Feb 2026 at 17:38
1,31 GB
MPEG-4 movie
Refinement & P1 debugging 2026-02-16.mp4...
|
73421
|
|
73422
|
NULL
|
0
|
2026-04-23T07:36:02.197761+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776929762197_m1.jpg...
|
Finder
|
Work
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Favourites
jiminny
AirDrop
Recents
Applications
Do Favourites
jiminny
AirDrop
Recents
Applications
Documents
Downloads
lukas
iCloud
iCloud Drive
Sync folder
Locations
DXP4800PLUS-B5F
Eject
Network
Tags
CRM
Orange
Red
Yellow
Green
Blue
Purple
All Tags…
Name
Date Modified
Size
Kind
2026
Today at 10:32
--
Folder
CleanShot 2026-04-23 at 09.45.36.mp4
Today at 10:32
1,74 GB
MPEG-4 movie
Daily 2026-04-22.mp4
Yesterday at 10:21
1,36 GB
MPEG-4 movie
Refinement 2026-04-06.mp4
21 Apr 2026 at 11:02
2,41 GB
MPEG-4 movie
Daily 2026-04-21.mp4
21 Apr 2026 at 10:00
567,8 MB
MPEG-4 movie
Refinement 2026-04-20.mp4
20 Apr 2026 at 16:56
4,25 GB
MPEG-4 movie
Daily 2026-04-20.mp4
20 Apr 2026 at 10:06
698,5 MB
MPEG-4 movie
Daily 2026-04-17.mp4
17 Apr 2026 at 10:16
1,16 GB
MPEG-4 movie
Daily 2026-04-16.mp4
16 Apr 2026 at 10:00
513,4 MB
MPEG-4 movie
Planning 2026-04-15.mp4
15 Apr 2026 at 11:14
2,75 GB
MPEG-4 movie
Retro 2026-04-14.mp4
14 Apr 2026 at 17:37
1,44 GB
MPEG-4 movie
Daily 2026-04-14.mp4
14 Apr 2026 at 10:09
924,4 MB
MPEG-4 movie
User pilot (Adi) 2026-04-09.mp4
9 Apr 2026 at 14:47
362,6 MB
MPEG-4 movie
Daily 2026-04-09.mp4
9 Apr 2026 at 10:07
748,8 MB
MPEG-4 movie
Daily 2026-04-08.mp4
8 Apr 2026 at 10:13
1,04 GB
MPEG-4 movie
Daily 2026-04-07.mp4
7 Apr 2026 at 10:01
575,5 MB
MPEG-4 movie
Daily 2026-04-06.mp4
6 Apr 2026 at 10:08
720,5 MB
MPEG-4 movie
Daily 2026-04-03.mp4
3 Apr 2026 at 10:21
1,02 GB
MPEG-4 movie
Planning 2026-04-01 & task split.mp4
1 Apr 2026 at 12:20
4,68 GB
MPEG-4 movie
Retro 2026-03-31.mp4
31 Mar 2026 at 18:29
3,4 GB
MPEG-4 movie
Daily 2026-03-31.mp4
31 Mar 2026 at 10:10
923,6 MB
MPEG-4 movie
Refinement 2026-03-30.mp4
30 Mar 2026 at 17:12
2,77 GB
MPEG-4 movie
Daily 2026-03-30.mp4
30 Mar 2026 at 10:05
641,8 MB
MPEG-4 movie
Daily 2026-03-27.mp4
27 Mar 2026 at 10:09
884,3 MB
MPEG-4 movie
Daily 2026-03-26.mp4
26 Mar 2026 at 9:59
476,6 MB
MPEG-4 movie
Daily 2026-03-24.mp4
24 Mar 2026 at 10:00
550,8 MB
MPEG-4 movie
Refinement 2026-03-23.mp4
23 Mar 2026 at 17:03
3,44 GB
MPEG-4 movie
Daily 2026-03-23.mp4
23 Mar 2026 at 10:00
438,9 MB
MPEG-4 movie
BE chapter 2026-03-20.mp4
20 Mar 2026 at 11:46
1,68 GB
MPEG-4 movie
Daily 2026-03-20.mp4
20 Mar 2026 at 10:06
430,4 MB
MPEG-4 movie
Planing 2026-03-18-converted.mp4
19 Mar 2026 at 12:01
2,38 GB
MPEG-4 movie
Refinement 2026-02-09-converted.mp4
19 Mar 2026 at 11:35
2,26 GB
MPEG-4 movie
Daily 2026-03-19.mp4
19 Mar 2026 at 9:57
386,3 MB
MPEG-4 movie
Review 2026-03-18.mp4
18 Mar 2026 at 16:20
705,8 MB
MPEG-4 movie
Planing 2026-03-18.mp4
18 Mar 2026 at 11:14
2,78 GB
MPEG-4 movie
Retro 2026-03-17.mp4
17 Mar 2026 at 17:40
1,53 GB
MPEG-4 movie
Daily 2026-03-17.mp4
17 Mar 2026 at 10:18
1,2 GB
MPEG-4 movie
Refinement 2026-03-16.mp4
16 Mar 2026 at 16:55
4,19 GB
MPEG-4 movie
Daily 2026-03-16.mp4
16 Mar 2026 at 10:02
592,2 MB
MPEG-4 movie
Daily 2026-03-13.mp4
13 Mar 2026 at 10:12
1,02 GB
MPEG-4 movie
1-1 2026-03-12.mp4
12 Mar 2026 at 18:35
637,6 MB
MPEG-4 movie
Daily 2026-03-12.mp4
12 Mar 2026 at 10:10
978,7 MB
MPEG-4 movie
Daily 2026-03-11.mp4
11 Mar 2026 at 10:06
798,7 MB
MPEG-4 movie
Daily 2026-03-10.mp4
10 Mar 2026 at 9:57
404,6 MB
MPEG-4 movie
Refinement 2026-03-09.mp4
9 Mar 2026 at 17:04
4,16 GB
MPEG-4 movie
Daily 2026-03-09.mp4
9 Mar 2026 at 9:56
319,7 MB
MPEG-4 movie
Daily 2026-03-06.mp4
6 Mar 2026 at 9:57
291,7 MB
MPEG-4 movie
Planning 2026-03-04.mp4
4 Mar 2026 at 11:09
2,62 GB
MPEG-4 movie
Daily 2026-03-02.mp4
2 Mar 2026 at 10:07
768,5 MB
MPEG-4 movie
Daily 2026-02-27.mp4
27 Feb 2026 at 10:02
546,8 MB
MPEG-4 movie
Daily 2026-02-26.mov
26 Feb 2026 at 9:53
96,6 MB
QT movie
Daily 2026-02-25.mov
25 Feb 2026 at 9:59
503,5 MB
QT movie
Opportunity-Contacts 2026-02-24.mp4
24 Feb 2026 at 12:03
791,7 MB
MPEG-4 movie
Daily 2026-02-24.mp4
24 Feb 2026 at 10:02
520,7 MB
MPEG-4 movie
Refinement 2026-02-23.mov
23 Feb 2026 at 16:31
2 GB
QT movie
Daily 2026-02-20 & Ani.mp4
20 Feb 2026 at 10:53
2,52 GB
MPEG-4 movie
Daily 2026-02-19.mp4
19 Feb 2026 at 9:54
234,2 MB
MPEG-4 movie
Review 2026-02-18.mp4
18 Feb 2026 at 16:25
925,1 MB
MPEG-4 movie
Planning 2026-02-18.mp4
18 Feb 2026 at 10:57
2,04 GB
MPEG-4 movie
Retro 2026-02-17.mp4
17 Feb 2026 at 17:38
1,31 GB
MPEG-4 movie
Refinement & P1 debugging 2026-02-16.mp4
16 Feb 2026 at 17:36
4,53 GB
MPEG-4 movie
SyncObjects3 2026-02-16.mp4
16 Feb 2026 at 15:10
1,42 GB
MPEG-4 movie...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Favourites","depth":6,"automation_id":"xSidebarHeader","role_description":"text"},{"role":"AXStaticText","text":"jiminny","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"AirDrop","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"Recents","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"Applications","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"Documents","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"Downloads","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"lukas","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"iCloud","depth":6,"automation_id":"xSidebarHeader","role_description":"text"},{"role":"AXStaticText","text":"iCloud Drive","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"Sync folder","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"Locations","depth":6,"automation_id":"xSidebarHeader","role_description":"text"},{"role":"AXStaticText","text":"DXP4800PLUS-B5F","depth":6,"role_description":"text"},{"role":"AXButton","text":"Eject","depth":6,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"Network","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"Tags","depth":6,"automation_id":"xSidebarHeader","role_description":"text"},{"role":"AXStaticText","text":"CRM","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"Orange","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"Red","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"Yellow","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"Green","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"Blue","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"Purple","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"All Tags…","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"Name","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"Date Modified","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"Size","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"Kind","depth":7,"role_description":"text"},{"role":"AXTextField","text":"2026","depth":7,"value":"2026","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Today at 10:32","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"--","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"Folder","depth":7,"role_description":"text"},{"role":"AXTextField","text":"CleanShot 2026-04-23 at 09.45.36.mp4","depth":7,"value":"CleanShot 2026-04-23 at 09.45.36.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Today at 10:32","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"1,74 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-22.mp4","depth":7,"value":"Daily 2026-04-22.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Yesterday at 10:21","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"1,36 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-04-06.mp4","depth":7,"value":"Refinement 2026-04-06.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"21 Apr 2026 at 11:02","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"2,41 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-21.mp4","depth":7,"value":"Daily 2026-04-21.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"21 Apr 2026 at 10:00","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"567,8 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-04-20.mp4","depth":7,"value":"Refinement 2026-04-20.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"20 Apr 2026 at 16:56","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"4,25 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-20.mp4","depth":7,"value":"Daily 2026-04-20.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"20 Apr 2026 at 10:06","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"698,5 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-17.mp4","depth":7,"value":"Daily 2026-04-17.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"17 Apr 2026 at 10:16","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"1,16 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-16.mp4","depth":7,"value":"Daily 2026-04-16.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"16 Apr 2026 at 10:00","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"513,4 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Planning 2026-04-15.mp4","depth":7,"value":"Planning 2026-04-15.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"15 Apr 2026 at 11:14","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"2,75 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Retro 2026-04-14.mp4","depth":7,"value":"Retro 2026-04-14.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"14 Apr 2026 at 17:37","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"1,44 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-14.mp4","depth":7,"value":"Daily 2026-04-14.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"14 Apr 2026 at 10:09","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"924,4 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"User pilot (Adi) 2026-04-09.mp4","depth":7,"value":"User pilot (Adi) 2026-04-09.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"9 Apr 2026 at 14:47","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"362,6 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-09.mp4","depth":7,"value":"Daily 2026-04-09.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"9 Apr 2026 at 10:07","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"748,8 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-08.mp4","depth":7,"value":"Daily 2026-04-08.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"8 Apr 2026 at 10:13","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"1,04 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-07.mp4","depth":7,"value":"Daily 2026-04-07.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"7 Apr 2026 at 10:01","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"575,5 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-06.mp4","depth":7,"value":"Daily 2026-04-06.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"6 Apr 2026 at 10:08","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"720,5 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-04-03.mp4","depth":7,"value":"Daily 2026-04-03.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"3 Apr 2026 at 10:21","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"1,02 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Planning 2026-04-01 & task split.mp4","depth":7,"value":"Planning 2026-04-01 & task split.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"1 Apr 2026 at 12:20","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"4,68 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Retro 2026-03-31.mp4","depth":7,"value":"Retro 2026-03-31.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"31 Mar 2026 at 18:29","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"3,4 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-31.mp4","depth":7,"value":"Daily 2026-03-31.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"31 Mar 2026 at 10:10","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"923,6 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-03-30.mp4","depth":7,"value":"Refinement 2026-03-30.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"30 Mar 2026 at 17:12","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"2,77 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-30.mp4","depth":7,"value":"Daily 2026-03-30.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"30 Mar 2026 at 10:05","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"641,8 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-27.mp4","depth":7,"value":"Daily 2026-03-27.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"27 Mar 2026 at 10:09","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"884,3 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-26.mp4","depth":7,"value":"Daily 2026-03-26.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"26 Mar 2026 at 9:59","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"476,6 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-24.mp4","depth":7,"value":"Daily 2026-03-24.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"24 Mar 2026 at 10:00","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"550,8 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-03-23.mp4","depth":7,"value":"Refinement 2026-03-23.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"23 Mar 2026 at 17:03","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"3,44 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-23.mp4","depth":7,"value":"Daily 2026-03-23.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"23 Mar 2026 at 10:00","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"438,9 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"BE chapter 2026-03-20.mp4","depth":7,"value":"BE chapter 2026-03-20.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"20 Mar 2026 at 11:46","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"1,68 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-20.mp4","depth":7,"value":"Daily 2026-03-20.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"20 Mar 2026 at 10:06","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"430,4 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Planing 2026-03-18-converted.mp4","depth":7,"value":"Planing 2026-03-18-converted.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"19 Mar 2026 at 12:01","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"2,38 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-02-09-converted.mp4","depth":7,"value":"Refinement 2026-02-09-converted.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"19 Mar 2026 at 11:35","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"2,26 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-19.mp4","depth":7,"value":"Daily 2026-03-19.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"19 Mar 2026 at 9:57","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"386,3 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Review 2026-03-18.mp4","depth":7,"value":"Review 2026-03-18.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"18 Mar 2026 at 16:20","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"705,8 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Planing 2026-03-18.mp4","depth":7,"value":"Planing 2026-03-18.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"18 Mar 2026 at 11:14","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"2,78 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Retro 2026-03-17.mp4","depth":7,"value":"Retro 2026-03-17.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"17 Mar 2026 at 17:40","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"1,53 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-17.mp4","depth":7,"value":"Daily 2026-03-17.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"17 Mar 2026 at 10:18","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"1,2 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-03-16.mp4","depth":7,"value":"Refinement 2026-03-16.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"16 Mar 2026 at 16:55","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"4,19 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-16.mp4","depth":7,"value":"Daily 2026-03-16.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"16 Mar 2026 at 10:02","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"592,2 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-13.mp4","depth":7,"value":"Daily 2026-03-13.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"13 Mar 2026 at 10:12","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"1,02 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"1-1 2026-03-12.mp4","depth":7,"value":"1-1 2026-03-12.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"12 Mar 2026 at 18:35","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"637,6 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-12.mp4","depth":7,"value":"Daily 2026-03-12.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"12 Mar 2026 at 10:10","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"978,7 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-11.mp4","depth":7,"value":"Daily 2026-03-11.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"11 Mar 2026 at 10:06","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"798,7 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-10.mp4","depth":7,"value":"Daily 2026-03-10.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"10 Mar 2026 at 9:57","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"404,6 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-03-09.mp4","depth":7,"value":"Refinement 2026-03-09.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"9 Mar 2026 at 17:04","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"4,16 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-09.mp4","depth":7,"value":"Daily 2026-03-09.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"9 Mar 2026 at 9:56","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"319,7 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-06.mp4","depth":7,"value":"Daily 2026-03-06.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"6 Mar 2026 at 9:57","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"291,7 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Planning 2026-03-04.mp4","depth":7,"value":"Planning 2026-03-04.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 Mar 2026 at 11:09","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"2,62 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-03-02.mp4","depth":7,"value":"Daily 2026-03-02.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"2 Mar 2026 at 10:07","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"768,5 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-02-27.mp4","depth":7,"value":"Daily 2026-02-27.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"27 Feb 2026 at 10:02","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"546,8 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-02-26.mov","depth":7,"value":"Daily 2026-02-26.mov","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"26 Feb 2026 at 9:53","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"96,6 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"QT movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-02-25.mov","depth":7,"value":"Daily 2026-02-25.mov","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"25 Feb 2026 at 9:59","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"503,5 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"QT movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Opportunity-Contacts 2026-02-24.mp4","depth":7,"value":"Opportunity-Contacts 2026-02-24.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"24 Feb 2026 at 12:03","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"791,7 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-02-24.mp4","depth":7,"bounds":{"left":0.6631944,"top":0.0,"width":0.10277778,"height":0.017777778},"value":"Daily 2026-02-24.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"24 Feb 2026 at 10:02","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"520,7 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Refinement 2026-02-23.mov","depth":7,"bounds":{"left":0.6631944,"top":0.0,"width":0.12916666,"height":0.017777778},"value":"Refinement 2026-02-23.mov","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"23 Feb 2026 at 16:31","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"2 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"QT movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-02-20 & Ani.mp4","depth":7,"bounds":{"left":0.6631944,"top":0.0,"width":0.12777779,"height":0.017777778},"value":"Daily 2026-02-20 & Ani.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"20 Feb 2026 at 10:53","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"2,52 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Daily 2026-02-19.mp4","depth":7,"bounds":{"left":0.6631944,"top":0.0,"width":0.10208333,"height":0.017777778},"value":"Daily 2026-02-19.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"19 Feb 2026 at 9:54","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"234,2 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Review 2026-02-18.mp4","depth":7,"bounds":{"left":0.6631944,"top":0.0,"width":0.11111111,"height":0.017777778},"value":"Review 2026-02-18.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"18 Feb 2026 at 16:25","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"925,1 MB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Planning 2026-02-18.mp4","depth":7,"bounds":{"left":0.6631944,"top":0.02111111,"width":0.11736111,"height":0.017777778},"value":"Planning 2026-02-18.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"18 Feb 2026 at 10:57","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"2,04 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Retro 2026-02-17.mp4","depth":7,"bounds":{"left":0.6631944,"top":0.043333333,"width":0.11736111,"height":0.017777778},"value":"Retro 2026-02-17.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"17 Feb 2026 at 17:38","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"1,31 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"Refinement & P1 debugging 2026-02-16.mp4","depth":7,"bounds":{"left":0.6631944,"top":0.06555556,"width":0.10208333,"height":0.017777778},"value":"Refinement & P1 debugging 2026-02-16.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"16 Feb 2026 at 17:36","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"4,53 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"},{"role":"AXTextField","text":"SyncObjects3 2026-02-16.mp4","depth":7,"bounds":{"left":0.6631944,"top":0.08777778,"width":0.19791667,"height":0.017777778},"value":"SyncObjects3 2026-02-16.mp4","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"16 Feb 2026 at 15:10","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"1,42 GB","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"MPEG-4 movie","depth":7,"role_description":"text"}]...
|
6036690097692163302
|
-2910781458352528949
|
visual_change
|
accessibility
|
NULL
|
Favourites
jiminny
AirDrop
Recents
Applications
Do Favourites
jiminny
AirDrop
Recents
Applications
Documents
Downloads
lukas
iCloud
iCloud Drive
Sync folder
Locations
DXP4800PLUS-B5F
Eject
Network
Tags
CRM
Orange
Red
Yellow
Green
Blue
Purple
All Tags…
Name
Date Modified
Size
Kind
2026
Today at 10:32
--
Folder
CleanShot 2026-04-23 at 09.45.36.mp4
Today at 10:32
1,74 GB
MPEG-4 movie
Daily 2026-04-22.mp4
Yesterday at 10:21
1,36 GB
MPEG-4 movie
Refinement 2026-04-06.mp4
21 Apr 2026 at 11:02
2,41 GB
MPEG-4 movie
Daily 2026-04-21.mp4
21 Apr 2026 at 10:00
567,8 MB
MPEG-4 movie
Refinement 2026-04-20.mp4
20 Apr 2026 at 16:56
4,25 GB
MPEG-4 movie
Daily 2026-04-20.mp4
20 Apr 2026 at 10:06
698,5 MB
MPEG-4 movie
Daily 2026-04-17.mp4
17 Apr 2026 at 10:16
1,16 GB
MPEG-4 movie
Daily 2026-04-16.mp4
16 Apr 2026 at 10:00
513,4 MB
MPEG-4 movie
Planning 2026-04-15.mp4
15 Apr 2026 at 11:14
2,75 GB
MPEG-4 movie
Retro 2026-04-14.mp4
14 Apr 2026 at 17:37
1,44 GB
MPEG-4 movie
Daily 2026-04-14.mp4
14 Apr 2026 at 10:09
924,4 MB
MPEG-4 movie
User pilot (Adi) 2026-04-09.mp4
9 Apr 2026 at 14:47
362,6 MB
MPEG-4 movie
Daily 2026-04-09.mp4
9 Apr 2026 at 10:07
748,8 MB
MPEG-4 movie
Daily 2026-04-08.mp4
8 Apr 2026 at 10:13
1,04 GB
MPEG-4 movie
Daily 2026-04-07.mp4
7 Apr 2026 at 10:01
575,5 MB
MPEG-4 movie
Daily 2026-04-06.mp4
6 Apr 2026 at 10:08
720,5 MB
MPEG-4 movie
Daily 2026-04-03.mp4
3 Apr 2026 at 10:21
1,02 GB
MPEG-4 movie
Planning 2026-04-01 & task split.mp4
1 Apr 2026 at 12:20
4,68 GB
MPEG-4 movie
Retro 2026-03-31.mp4
31 Mar 2026 at 18:29
3,4 GB
MPEG-4 movie
Daily 2026-03-31.mp4
31 Mar 2026 at 10:10
923,6 MB
MPEG-4 movie
Refinement 2026-03-30.mp4
30 Mar 2026 at 17:12
2,77 GB
MPEG-4 movie
Daily 2026-03-30.mp4
30 Mar 2026 at 10:05
641,8 MB
MPEG-4 movie
Daily 2026-03-27.mp4
27 Mar 2026 at 10:09
884,3 MB
MPEG-4 movie
Daily 2026-03-26.mp4
26 Mar 2026 at 9:59
476,6 MB
MPEG-4 movie
Daily 2026-03-24.mp4
24 Mar 2026 at 10:00
550,8 MB
MPEG-4 movie
Refinement 2026-03-23.mp4
23 Mar 2026 at 17:03
3,44 GB
MPEG-4 movie
Daily 2026-03-23.mp4
23 Mar 2026 at 10:00
438,9 MB
MPEG-4 movie
BE chapter 2026-03-20.mp4
20 Mar 2026 at 11:46
1,68 GB
MPEG-4 movie
Daily 2026-03-20.mp4
20 Mar 2026 at 10:06
430,4 MB
MPEG-4 movie
Planing 2026-03-18-converted.mp4
19 Mar 2026 at 12:01
2,38 GB
MPEG-4 movie
Refinement 2026-02-09-converted.mp4
19 Mar 2026 at 11:35
2,26 GB
MPEG-4 movie
Daily 2026-03-19.mp4
19 Mar 2026 at 9:57
386,3 MB
MPEG-4 movie
Review 2026-03-18.mp4
18 Mar 2026 at 16:20
705,8 MB
MPEG-4 movie
Planing 2026-03-18.mp4
18 Mar 2026 at 11:14
2,78 GB
MPEG-4 movie
Retro 2026-03-17.mp4
17 Mar 2026 at 17:40
1,53 GB
MPEG-4 movie
Daily 2026-03-17.mp4
17 Mar 2026 at 10:18
1,2 GB
MPEG-4 movie
Refinement 2026-03-16.mp4
16 Mar 2026 at 16:55
4,19 GB
MPEG-4 movie
Daily 2026-03-16.mp4
16 Mar 2026 at 10:02
592,2 MB
MPEG-4 movie
Daily 2026-03-13.mp4
13 Mar 2026 at 10:12
1,02 GB
MPEG-4 movie
1-1 2026-03-12.mp4
12 Mar 2026 at 18:35
637,6 MB
MPEG-4 movie
Daily 2026-03-12.mp4
12 Mar 2026 at 10:10
978,7 MB
MPEG-4 movie
Daily 2026-03-11.mp4
11 Mar 2026 at 10:06
798,7 MB
MPEG-4 movie
Daily 2026-03-10.mp4
10 Mar 2026 at 9:57
404,6 MB
MPEG-4 movie
Refinement 2026-03-09.mp4
9 Mar 2026 at 17:04
4,16 GB
MPEG-4 movie
Daily 2026-03-09.mp4
9 Mar 2026 at 9:56
319,7 MB
MPEG-4 movie
Daily 2026-03-06.mp4
6 Mar 2026 at 9:57
291,7 MB
MPEG-4 movie
Planning 2026-03-04.mp4
4 Mar 2026 at 11:09
2,62 GB
MPEG-4 movie
Daily 2026-03-02.mp4
2 Mar 2026 at 10:07
768,5 MB
MPEG-4 movie
Daily 2026-02-27.mp4
27 Feb 2026 at 10:02
546,8 MB
MPEG-4 movie
Daily 2026-02-26.mov
26 Feb 2026 at 9:53
96,6 MB
QT movie
Daily 2026-02-25.mov
25 Feb 2026 at 9:59
503,5 MB
QT movie
Opportunity-Contacts 2026-02-24.mp4
24 Feb 2026 at 12:03
791,7 MB
MPEG-4 movie
Daily 2026-02-24.mp4
24 Feb 2026 at 10:02
520,7 MB
MPEG-4 movie
Refinement 2026-02-23.mov
23 Feb 2026 at 16:31
2 GB
QT movie
Daily 2026-02-20 & Ani.mp4
20 Feb 2026 at 10:53
2,52 GB
MPEG-4 movie
Daily 2026-02-19.mp4
19 Feb 2026 at 9:54
234,2 MB
MPEG-4 movie
Review 2026-02-18.mp4
18 Feb 2026 at 16:25
925,1 MB
MPEG-4 movie
Planning 2026-02-18.mp4
18 Feb 2026 at 10:57
2,04 GB
MPEG-4 movie
Retro 2026-02-17.mp4
17 Feb 2026 at 17:38
1,31 GB
MPEG-4 movie
Refinement & P1 debugging 2026-02-16.mp4
16 Feb 2026 at 17:36
4,53 GB
MPEG-4 movie
SyncObjects3 2026-02-16.mp4
16 Feb 2026 at 15:10
1,42 GB
MPEG-4 movie...
|
73420
|
|
73391
|
NULL
|
0
|
2026-04-23T07:31:21.430133+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776929481430_m2.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsRepository.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
17
7
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active Ask Jiminny reports whose expiry date has passed.
*
* @return Collection<AutomatedReport>
*/
public function getExpiredActiveAskJiminnyReports(): Collection
{
return AutomatedReport::where('status', true)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereNotNull('expires_at')
->where('expires_at', '<', now()->toDateString())
->get();
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('report_id', $report->getId())
->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])
->latest()
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
public function countAllUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Restrict a query on the automated_reports table to reports the given user is allowed to see.
*
* Matches the customer-facing audience:
* - explicit user recipients (recipients.users)
* - members of any of the report's groups (Ask Jiminny reports)
*/
private function applyUserAccessScope(Builder $query, User $user): void
{
$userId = $user->getId();
$groupId = $user->getGroupId();
$query
->where('automated_reports.team_id', $user->getTeamId())
->where(function (Builder $q) use ($userId, $groupId): void {
$q->whereJsonContains('automated_reports.recipients->users', $userId);
if ($groupId !== null) {
$q->orWhere(function (Builder $sub) use ($groupId): void {
$sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereJsonContains('automated_reports.groups', $groupId);
});
}
});
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
36
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\DealInsights;
use Doctrine\DBAL\Connection;
use Generator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Jiminny\Component\DealInsights\Forecast\DealData;
use Jiminny\Component\DealInsights\Forecast\DealsFilter;
use Jiminny\Component\DealInsights\QueryBuilder\QueryBuilder;
use Jiminny\Component\DealInsights\QueryBuilder\Visitor\QueryBuilderVisitorInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Services\Crm\IntegrationApp\DTO\Utils\UrlGeneratorInterface;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class DealsRepository implements DealsRepositoryInterface
{
private Connection $connection;
private ProviderRegistry $providerRegistry;
/**
* @var QueryBuilderVisitorInterface[]
*/
private array $visitors = [];
/**
* @param QueryBuilderVisitorInterface[] $visitors
*/
public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])
{
$this->connection = $connection;
$this->providerRegistry = $crmProviderRegistry;
foreach ($visitors as $visitor) {
$this->visitors[$visitor->getIdentifier()] = $visitor;
}
}
public function getDeals(CriteriaInterface $criteria): array
{
$context = $criteria->getContext();
$team = $context->getTeam();
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$this->visit($qb, $criteria);
return $this->execute($team, $crmService, $qb);
}
public function getDeal(Team $team, int $id): array
{
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$qb->andWhere('opp.id = :id')->setParameter('id', $id);
return $this->execute($team, $crmService, $qb);
}
public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])
{
$qb = new QueryBuilder($this->connection);
$qb
->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')
->from('crm_fields', 'f')
->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')
->where('f.crm_configuration_id = :crm')
->andWhere('f.object_type = :type')
->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')
->orderBy('fd.object_id', 'ASC')
->addOrderBy('fd.updated_at', 'ASC')
->setParameter('type', Field::OBJECT_OPPORTUNITY)
->setParameter('crm', $crmId)
;
if (! empty($crmFields)) {
$fields = array_map(fn ($value): string => '"' . $value . '"', $crmFields);
$qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');
}
return $qb->executeQuery()->fetchAllAssociative();
}
public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAssociative();
}
public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('COALESCE(opp.currency_code, "' . $defaultCurrency . '") AS currency')
->addSelect('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
->groupBy('currency')
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAllAssociative();
}
public function getDealActivities(CriteriaInterface $criteria): array
{
$qb = Activity::with(['participants', 'user'])
->where('opportunity_id', $criteria->getOpportunityId())
->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())
->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())
->orderBy($criteria->getSortBy(), $criteria->getSortDirection())
;
// Should we filter activities by criteria? It's intended to filter deals.
return $qb->get()->all();
}
public function getStages(CriteriaInterface $criteria): array
{
$qb = new QueryBuilder($this->connection);
$qb
->select('id', 'label', 'sequence')
->from('stages', 's')
->where('crm_configuration_id = :crm_configuration_id')
->andWhere('type = :type')
->orderBy('sequence', 'ASC')
->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())
->setParameter('type', Stage::TYPE_OPPORTUNITY);
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$result[$row['id']] = [
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
public function getConfigurationStages(Configuration $configuration): Collection
{
return $configuration
->stages()
->where('type', Stage::TYPE_OPPORTUNITY)
->get();
}
public function getPipelineData(Configuration $crm): array
{
$qb = new QueryBuilder($this->connection);
$provider = $crm->provider;
$qb
->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')
->from('stages', 's')
->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')
->where('s.crm_configuration_id = :crm_configuration_id')
->andWhere('s.type = :type')
->orderBy('bps.business_process_id', 'ASC')
->addOrderBy('s.sequence', 'ASC')
->setParameter('crm_configuration_id', $crm->id)
->setParameter('type', Stage::TYPE_OPPORTUNITY)
;
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];
$result[$row['pipeline_id']][] = [
'value' => $value,
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
private function createQueryBuilder(string $realm): QueryBuilder
{
return (new QueryBuilder($this->connection))
->setRealm($realm)
->from('opportunities', 'opp')
->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')
->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')
->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')
;
}
/**
* Applies all applicable visitors and returns the IDs of the executed ones
*
* @return string[]
*/
private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array
{
$queryVisitors = [];
foreach ($this->visitors as $visitor) {
if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {
$visitor->visit($queryBuilder, $criteria);
$queryVisitors[] = $visitor->getIdentifier();
}
}
return $queryVisitors;
}
private function hydrateStages(array $deals): array
{
foreach ($this->fetchStages(array_keys($deals)) as $stage) {
$oppId = (int) $stage['opportunity_id'];
if (! isset($deals[$oppId])) {
continue; // or throw??!
}
$deals[$oppId]['stages'][] = [
'id' => $stage['stage_id'],
'name' => $stage['label'],
'enteredAt' => $stage['created_at'],
];
}
return $deals;
}
/**
* @param int[] $dealIds
*/
private function fetchStages(array $dealIds): array
{
if (empty($dealIds)) {
return [];
}
$qb = new QueryBuilder($this->connection);
$qb
->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')
->from('opportunity_stages', 'os')
->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')
->where($qb->expr()->in('os.opportunity_id', $dealIds))
->orderBy('os.opportunity_id', 'ASC')
->addOrderBy('s.created_at', 'ASC')
;
return $qb->executeQuery()->fetchAllAssociative();
}
private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array
{
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$data = [
'uuid' => RequiresUUID::toNormal($row['uuid']),
'name' => $row['name'],
'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),
'account' => [
'name' => $row['acc_name'],
'url' => $crmService->generateProviderUrl(
providerId: $row['acc_provider_id'],
objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'
),
],
'owner' => null,
'rawValue' => [
'amount' => (float) $row['value'],
'currency' => $row['currency_code'],
],
'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),
'openDate' => $row['remotely_created_at'] ?? null,
'closeDate' => $row['close_date'] ?? null,
'stages' => [],
'currentPipelineId' => $row['pipeline_id'],
'currentStage' => [
'id' => $row['stage_id'],
'enteredAt' => $row['stage_updated_at'],
],
'currentStageUpdatedAt' => $row['stage_updated_at'],
'isClosed' => (bool) $row['is_closed'],
'isWon' => (bool) $row['is_won'],
];
if (isset($row['owner_uuid'])) {
$data['owner'] = [
'uuid' => RequiresUUID::toNormal($row['owner_uuid']),
'name' => $row['owner_name'],
'photoUrl' => $row['owner_photo'] === null
? null
: client_cdn($row['owner_photo'], $team),
'id' => $row['owner_id'],
'job' => $row['owner_job'],
];
}
$result[(int) $row['opp_id']] = $data;
}
return $this->hydrateStages($result);
}
private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder
{
$qb = clone $queryBuilder;
$qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');
$qb
->select(...[
'opp.id as opp_id',
'opp.uuid',
'opp.name',
'opp.value',
'opp.currency_code',
'opp.close_date',
'opp.remotely_created_at',
'opp.is_closed',
'opp.is_won',
])
->addSelect(...[
'usr.uuid as owner_uuid',
'usr.name AS owner_name',
'usr.photo_path as owner_photo',
'usr.id AS owner_id',
'jt.name as owner_job',
])
->addSelect('opp.stage_id', 'opp.stage_updated_at')
->addSelect(...[
'acc.name AS acc_name',
'acc.is_internal as acc_is_internal',
'opp.stage_updated_at',
'acc.crm_provider_id AS acc_provider_id',
'opp.crm_provider_id AS opp_provider_id',
])
->addSelect('rt.business_process_id AS pipeline_id')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'));
return $qb;
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws SocialAccountTokenInvalidException
*/
private function getCrmService(Team $team): ServiceInterface
{
$crmService = $this->providerRegistry->get($team->crm->provider);
$crmService->setConfiguration($team->crm);
if ($crmService instanceof UrlGeneratorInterface) {
$crmService->setCrmUrlGenerator($team->crm);
}
return $crmService;
}
/**
*
* @return Generator<DealData>
*/
public function getForecastData(DealsFilter $filter): Generator
{
$opportunities = DB::query()
->select([
'o.value',
'o.close_date',
'o.currency_code',
'o.is_won',
'o.is_closed',
'o.probability',
'o.forecast_category',
])
->from('opportunities', 'o')
->join('users', 'users.id', '=', 'o.user_id')
->join('groups', 'groups.id', '=', 'users.group_id')
->where('users.team_id', $filter->getTeam()->getId())
->where('o.close_date', '>=', $filter->getStartDate())
->where('o.close_date', '<=', $filter->getEndDate())
->where('o.currency_code', $filter->getCurrency())
->where('o.deleted_at', '=', null)
;
$userUuidList = $filter->getUserUuidList();
if (! empty($userUuidList)) {
$userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);
$opportunities->whereIn('users.uuid', $userUuidList);
}
$groupUuidList = $filter->getGroupUuidList();
if (! empty($groupUuidList)) {
$groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);
$opportunities->whereIn('groups.uuid', $groupUuidList);
}
foreach ($opportunities->cursor() as $row) {
yield new DealData(
(float) $row->value,
$row->close_date,
! empty($row->is_won),
! empty($row->is_closed),
$row->probability ?: 0,
$row->forecast_category ?: '',
);
}
}
public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection
{
return $user->subscriptionSets()
->where(static function (Eloquent\Builder $query): void {
$query
->whereNull('expired_at')
->orWhere('expired_at', '>=', now());
})
->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {
$join
->on('subscription_set_id', '=', 'activity_subscription_sets.id');
$join
->where('followable_type', Models\Activity\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)
->whereIn('followable_id', $opportunityIds);
})
->pluck('followable_id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.25731382,"top":0.019952115,"width":0.03856383,"height":0.025538707},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20157-AJ-report-not-send-notification, menu","depth":5,"bounds":{"left":0.29587767,"top":0.019952115,"width":0.10139628,"height":0.025538707},"help_text":"Git Branch: JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.7972075,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"RequestGenerateAskJiminnyReportJobTest","depth":6,"bounds":{"left":0.8125,"top":0.019952115,"width":0.10305851,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"17","depth":4,"bounds":{"left":0.60339093,"top":0.17478053,"width":0.00930851,"height":0.015163607},"role_description":"text"},{"role":"AXStaticText","text":"7","depth":4,"bounds":{"left":0.6146942,"top":0.17478053,"width":0.0076462766,"height":0.015163607},"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.6243351,"top":0.17478053,"width":0.00731383,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.63331115,"top":0.17318435,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.640625,"top":0.17318435,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active Ask Jiminny reports whose expiry date has passed.\n *\n * @return Collection<AutomatedReport>\n */\n public function getExpiredActiveAskJiminnyReports(): Collection\n {\n return AutomatedReport::where('status', true)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereNotNull('expires_at')\n ->where('expires_at', '<', now()->toDateString())\n ->get();\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('report_id', $report->getId())\n ->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])\n ->latest()\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n \n public function countAllUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Restrict a query on the automated_reports table to reports the given user is allowed to see.\n *\n * Matches the customer-facing audience:\n * - explicit user recipients (recipients.users)\n * - members of any of the report's groups (Ask Jiminny reports)\n */\n private function applyUserAccessScope(Builder $query, User $user): void\n {\n $userId = $user->getId();\n $groupId = $user->getGroupId();\n\n $query\n ->where('automated_reports.team_id', $user->getTeamId())\n ->where(function (Builder $q) use ($userId, $groupId): void {\n $q->whereJsonContains('automated_reports.recipients->users', $userId);\n\n if ($groupId !== null) {\n $q->orWhere(function (Builder $sub) use ($groupId): void {\n $sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereJsonContains('automated_reports.groups', $groupId);\n });\n }\n });\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active Ask Jiminny reports whose expiry date has passed.\n *\n * @return Collection<AutomatedReport>\n */\n public function getExpiredActiveAskJiminnyReports(): Collection\n {\n return AutomatedReport::where('status', true)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereNotNull('expires_at')\n ->where('expires_at', '<', now()->toDateString())\n ->get();\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('report_id', $report->getId())\n ->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])\n ->latest()\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n \n public function countAllUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Restrict a query on the automated_reports table to reports the given user is allowed to see.\n *\n * Matches the customer-facing audience:\n * - explicit user recipients (recipients.users)\n * - members of any of the report's groups (Ask Jiminny reports)\n */\n private function applyUserAccessScope(Builder $query, User $user): void\n {\n $userId = $user->getId();\n $groupId = $user->getGroupId();\n\n $query\n ->where('automated_reports.team_id', $user->getTeamId())\n ->where(function (Builder $q) use ($userId, $groupId): void {\n $q->whereJsonContains('automated_reports.recipients->users', $userId);\n\n if ($groupId !== null) {\n $q->orWhere(function (Builder $sub) use ($groupId): void {\n $sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereJsonContains('automated_reports.groups', $groupId);\n });\n }\n });\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"36","depth":4,"bounds":{"left":0.69980055,"top":0.2490024,"width":0.010305851,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7117686,"top":0.24740623,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7190825,"top":0.24740623,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\DealInsights;\n\nuse Doctrine\\DBAL\\Connection;\nuse Generator;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealData;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealsFilter;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\QueryBuilder;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\Visitor\\QueryBuilderVisitorInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Services\\Crm\\IntegrationApp\\DTO\\Utils\\UrlGeneratorInterface;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Eloquent;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass DealsRepository implements DealsRepositoryInterface\n{\n private Connection $connection;\n\n private ProviderRegistry $providerRegistry;\n\n /**\n * @var QueryBuilderVisitorInterface[]\n */\n private array $visitors = [];\n\n /**\n * @param QueryBuilderVisitorInterface[] $visitors\n */\n public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])\n {\n $this->connection = $connection;\n $this->providerRegistry = $crmProviderRegistry;\n\n foreach ($visitors as $visitor) {\n $this->visitors[$visitor->getIdentifier()] = $visitor;\n }\n }\n\n public function getDeals(CriteriaInterface $criteria): array\n {\n $context = $criteria->getContext();\n $team = $context->getTeam();\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n\n $this->visit($qb, $criteria);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getDeal(Team $team, int $id): array\n {\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n $qb->andWhere('opp.id = :id')->setParameter('id', $id);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')\n ->from('crm_fields', 'f')\n ->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')\n ->where('f.crm_configuration_id = :crm')\n ->andWhere('f.object_type = :type')\n ->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')\n ->orderBy('fd.object_id', 'ASC')\n ->addOrderBy('fd.updated_at', 'ASC')\n\n ->setParameter('type', Field::OBJECT_OPPORTUNITY)\n ->setParameter('crm', $crmId)\n ;\n\n if (! empty($crmFields)) {\n $fields = array_map(fn ($value): string => '\"' . $value . '\"', $crmFields);\n $qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');\n }\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAssociative();\n }\n\n public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('COALESCE(opp.currency_code, \"' . $defaultCurrency . '\") AS currency')\n ->addSelect('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ->groupBy('currency')\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getDealActivities(CriteriaInterface $criteria): array\n {\n $qb = Activity::with(['participants', 'user'])\n ->where('opportunity_id', $criteria->getOpportunityId())\n ->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())\n ->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())\n ->orderBy($criteria->getSortBy(), $criteria->getSortDirection())\n ;\n\n // Should we filter activities by criteria? It's intended to filter deals.\n\n return $qb->get()->all();\n }\n\n public function getStages(CriteriaInterface $criteria): array\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('id', 'label', 'sequence')\n ->from('stages', 's')\n ->where('crm_configuration_id = :crm_configuration_id')\n ->andWhere('type = :type')\n ->orderBy('sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())\n ->setParameter('type', Stage::TYPE_OPPORTUNITY);\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $result[$row['id']] = [\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n public function getConfigurationStages(Configuration $configuration): Collection\n {\n return $configuration\n ->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get();\n }\n\n public function getPipelineData(Configuration $crm): array\n {\n $qb = new QueryBuilder($this->connection);\n $provider = $crm->provider;\n\n $qb\n ->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')\n ->from('stages', 's')\n ->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')\n ->where('s.crm_configuration_id = :crm_configuration_id')\n ->andWhere('s.type = :type')\n ->orderBy('bps.business_process_id', 'ASC')\n ->addOrderBy('s.sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $crm->id)\n ->setParameter('type', Stage::TYPE_OPPORTUNITY)\n ;\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];\n $result[$row['pipeline_id']][] = [\n 'value' => $value,\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n private function createQueryBuilder(string $realm): QueryBuilder\n {\n return (new QueryBuilder($this->connection))\n ->setRealm($realm)\n ->from('opportunities', 'opp')\n ->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')\n ->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')\n ->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')\n ;\n }\n\n /**\n * Applies all applicable visitors and returns the IDs of the executed ones\n *\n * @return string[]\n */\n private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array\n {\n $queryVisitors = [];\n\n foreach ($this->visitors as $visitor) {\n if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {\n $visitor->visit($queryBuilder, $criteria);\n\n $queryVisitors[] = $visitor->getIdentifier();\n }\n }\n\n return $queryVisitors;\n }\n\n private function hydrateStages(array $deals): array\n {\n foreach ($this->fetchStages(array_keys($deals)) as $stage) {\n $oppId = (int) $stage['opportunity_id'];\n\n if (! isset($deals[$oppId])) {\n continue; // or throw??!\n }\n\n $deals[$oppId]['stages'][] = [\n 'id' => $stage['stage_id'],\n 'name' => $stage['label'],\n 'enteredAt' => $stage['created_at'],\n ];\n }\n\n return $deals;\n }\n\n /**\n * @param int[] $dealIds\n */\n private function fetchStages(array $dealIds): array\n {\n if (empty($dealIds)) {\n return [];\n }\n\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')\n ->from('opportunity_stages', 'os')\n ->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')\n ->where($qb->expr()->in('os.opportunity_id', $dealIds))\n ->orderBy('os.opportunity_id', 'ASC')\n ->addOrderBy('s.created_at', 'ASC')\n ;\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array\n {\n $result = [];\n\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $data = [\n 'uuid' => RequiresUUID::toNormal($row['uuid']),\n 'name' => $row['name'],\n 'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),\n 'account' => [\n 'name' => $row['acc_name'],\n 'url' => $crmService->generateProviderUrl(\n providerId: $row['acc_provider_id'],\n objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'\n ),\n ],\n 'owner' => null,\n 'rawValue' => [\n 'amount' => (float) $row['value'],\n 'currency' => $row['currency_code'],\n ],\n 'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),\n 'openDate' => $row['remotely_created_at'] ?? null,\n 'closeDate' => $row['close_date'] ?? null,\n 'stages' => [],\n 'currentPipelineId' => $row['pipeline_id'],\n 'currentStage' => [\n 'id' => $row['stage_id'],\n 'enteredAt' => $row['stage_updated_at'],\n ],\n 'currentStageUpdatedAt' => $row['stage_updated_at'],\n 'isClosed' => (bool) $row['is_closed'],\n 'isWon' => (bool) $row['is_won'],\n ];\n\n if (isset($row['owner_uuid'])) {\n $data['owner'] = [\n 'uuid' => RequiresUUID::toNormal($row['owner_uuid']),\n 'name' => $row['owner_name'],\n 'photoUrl' => $row['owner_photo'] === null\n ? null\n : client_cdn($row['owner_photo'], $team),\n 'id' => $row['owner_id'],\n 'job' => $row['owner_job'],\n ];\n }\n\n $result[(int) $row['opp_id']] = $data;\n }\n\n return $this->hydrateStages($result);\n }\n\n private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder\n {\n $qb = clone $queryBuilder;\n $qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');\n\n $qb\n ->select(...[\n 'opp.id as opp_id',\n 'opp.uuid',\n 'opp.name',\n 'opp.value',\n 'opp.currency_code',\n 'opp.close_date',\n 'opp.remotely_created_at',\n 'opp.is_closed',\n 'opp.is_won',\n ])\n ->addSelect(...[\n 'usr.uuid as owner_uuid',\n 'usr.name AS owner_name',\n 'usr.photo_path as owner_photo',\n 'usr.id AS owner_id',\n 'jt.name as owner_job',\n ])\n ->addSelect('opp.stage_id', 'opp.stage_updated_at')\n ->addSelect(...[\n 'acc.name AS acc_name',\n 'acc.is_internal as acc_is_internal',\n 'opp.stage_updated_at',\n 'acc.crm_provider_id AS acc_provider_id',\n 'opp.crm_provider_id AS opp_provider_id',\n ])\n ->addSelect('rt.business_process_id AS pipeline_id')\n\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'));\n\n return $qb;\n }\n\n /**\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws SocialAccountTokenInvalidException\n */\n private function getCrmService(Team $team): ServiceInterface\n {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n $crmService->setConfiguration($team->crm);\n if ($crmService instanceof UrlGeneratorInterface) {\n $crmService->setCrmUrlGenerator($team->crm);\n }\n\n return $crmService;\n }\n\n /**\n *\n * @return Generator<DealData>\n */\n public function getForecastData(DealsFilter $filter): Generator\n {\n $opportunities = DB::query()\n ->select([\n 'o.value',\n 'o.close_date',\n 'o.currency_code',\n 'o.is_won',\n 'o.is_closed',\n 'o.probability',\n 'o.forecast_category',\n ])\n ->from('opportunities', 'o')\n ->join('users', 'users.id', '=', 'o.user_id')\n ->join('groups', 'groups.id', '=', 'users.group_id')\n ->where('users.team_id', $filter->getTeam()->getId())\n ->where('o.close_date', '>=', $filter->getStartDate())\n ->where('o.close_date', '<=', $filter->getEndDate())\n ->where('o.currency_code', $filter->getCurrency())\n ->where('o.deleted_at', '=', null)\n ;\n\n $userUuidList = $filter->getUserUuidList();\n if (! empty($userUuidList)) {\n $userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);\n\n $opportunities->whereIn('users.uuid', $userUuidList);\n }\n\n $groupUuidList = $filter->getGroupUuidList();\n if (! empty($groupUuidList)) {\n $groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);\n\n $opportunities->whereIn('groups.uuid', $groupUuidList);\n }\n\n foreach ($opportunities->cursor() as $row) {\n yield new DealData(\n (float) $row->value,\n $row->close_date,\n ! empty($row->is_won),\n ! empty($row->is_closed),\n $row->probability ?: 0,\n $row->forecast_category ?: '',\n );\n }\n }\n\n public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection\n {\n return $user->subscriptionSets()\n ->where(static function (Eloquent\\Builder $query): void {\n $query\n ->whereNull('expired_at')\n ->orWhere('expired_at', '>=', now());\n })\n ->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n $join\n ->where('followable_type', Models\\Activity\\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)\n ->whereIn('followable_id', $opportunityIds);\n })\n ->pluck('followable_id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\DealInsights;\n\nuse Doctrine\\DBAL\\Connection;\nuse Generator;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealData;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealsFilter;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\QueryBuilder;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\Visitor\\QueryBuilderVisitorInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Services\\Crm\\IntegrationApp\\DTO\\Utils\\UrlGeneratorInterface;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Eloquent;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass DealsRepository implements DealsRepositoryInterface\n{\n private Connection $connection;\n\n private ProviderRegistry $providerRegistry;\n\n /**\n * @var QueryBuilderVisitorInterface[]\n */\n private array $visitors = [];\n\n /**\n * @param QueryBuilderVisitorInterface[] $visitors\n */\n public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])\n {\n $this->connection = $connection;\n $this->providerRegistry = $crmProviderRegistry;\n\n foreach ($visitors as $visitor) {\n $this->visitors[$visitor->getIdentifier()] = $visitor;\n }\n }\n\n public function getDeals(CriteriaInterface $criteria): array\n {\n $context = $criteria->getContext();\n $team = $context->getTeam();\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n\n $this->visit($qb, $criteria);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getDeal(Team $team, int $id): array\n {\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n $qb->andWhere('opp.id = :id')->setParameter('id', $id);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')\n ->from('crm_fields', 'f')\n ->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')\n ->where('f.crm_configuration_id = :crm')\n ->andWhere('f.object_type = :type')\n ->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')\n ->orderBy('fd.object_id', 'ASC')\n ->addOrderBy('fd.updated_at', 'ASC')\n\n ->setParameter('type', Field::OBJECT_OPPORTUNITY)\n ->setParameter('crm', $crmId)\n ;\n\n if (! empty($crmFields)) {\n $fields = array_map(fn ($value): string => '\"' . $value . '\"', $crmFields);\n $qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');\n }\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAssociative();\n }\n\n public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('COALESCE(opp.currency_code, \"' . $defaultCurrency . '\") AS currency')\n ->addSelect('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ->groupBy('currency')\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getDealActivities(CriteriaInterface $criteria): array\n {\n $qb = Activity::with(['participants', 'user'])\n ->where('opportunity_id', $criteria->getOpportunityId())\n ->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())\n ->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())\n ->orderBy($criteria->getSortBy(), $criteria->getSortDirection())\n ;\n\n // Should we filter activities by criteria? It's intended to filter deals.\n\n return $qb->get()->all();\n }\n\n public function getStages(CriteriaInterface $criteria): array\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('id', 'label', 'sequence')\n ->from('stages', 's')\n ->where('crm_configuration_id = :crm_configuration_id')\n ->andWhere('type = :type')\n ->orderBy('sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())\n ->setParameter('type', Stage::TYPE_OPPORTUNITY);\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $result[$row['id']] = [\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n public function getConfigurationStages(Configuration $configuration): Collection\n {\n return $configuration\n ->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get();\n }\n\n public function getPipelineData(Configuration $crm): array\n {\n $qb = new QueryBuilder($this->connection);\n $provider = $crm->provider;\n\n $qb\n ->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')\n ->from('stages', 's')\n ->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')\n ->where('s.crm_configuration_id = :crm_configuration_id')\n ->andWhere('s.type = :type')\n ->orderBy('bps.business_process_id', 'ASC')\n ->addOrderBy('s.sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $crm->id)\n ->setParameter('type', Stage::TYPE_OPPORTUNITY)\n ;\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];\n $result[$row['pipeline_id']][] = [\n 'value' => $value,\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n private function createQueryBuilder(string $realm): QueryBuilder\n {\n return (new QueryBuilder($this->connection))\n ->setRealm($realm)\n ->from('opportunities', 'opp')\n ->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')\n ->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')\n ->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')\n ;\n }\n\n /**\n * Applies all applicable visitors and returns the IDs of the executed ones\n *\n * @return string[]\n */\n private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array\n {\n $queryVisitors = [];\n\n foreach ($this->visitors as $visitor) {\n if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {\n $visitor->visit($queryBuilder, $criteria);\n\n $queryVisitors[] = $visitor->getIdentifier();\n }\n }\n\n return $queryVisitors;\n }\n\n private function hydrateStages(array $deals): array\n {\n foreach ($this->fetchStages(array_keys($deals)) as $stage) {\n $oppId = (int) $stage['opportunity_id'];\n\n if (! isset($deals[$oppId])) {\n continue; // or throw??!\n }\n\n $deals[$oppId]['stages'][] = [\n 'id' => $stage['stage_id'],\n 'name' => $stage['label'],\n 'enteredAt' => $stage['created_at'],\n ];\n }\n\n return $deals;\n }\n\n /**\n * @param int[] $dealIds\n */\n private function fetchStages(array $dealIds): array\n {\n if (empty($dealIds)) {\n return [];\n }\n\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')\n ->from('opportunity_stages', 'os')\n ->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')\n ->where($qb->expr()->in('os.opportunity_id', $dealIds))\n ->orderBy('os.opportunity_id', 'ASC')\n ->addOrderBy('s.created_at', 'ASC')\n ;\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array\n {\n $result = [];\n\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $data = [\n 'uuid' => RequiresUUID::toNormal($row['uuid']),\n 'name' => $row['name'],\n 'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),\n 'account' => [\n 'name' => $row['acc_name'],\n 'url' => $crmService->generateProviderUrl(\n providerId: $row['acc_provider_id'],\n objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'\n ),\n ],\n 'owner' => null,\n 'rawValue' => [\n 'amount' => (float) $row['value'],\n 'currency' => $row['currency_code'],\n ],\n 'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),\n 'openDate' => $row['remotely_created_at'] ?? null,\n 'closeDate' => $row['close_date'] ?? null,\n 'stages' => [],\n 'currentPipelineId' => $row['pipeline_id'],\n 'currentStage' => [\n 'id' => $row['stage_id'],\n 'enteredAt' => $row['stage_updated_at'],\n ],\n 'currentStageUpdatedAt' => $row['stage_updated_at'],\n 'isClosed' => (bool) $row['is_closed'],\n 'isWon' => (bool) $row['is_won'],\n ];\n\n if (isset($row['owner_uuid'])) {\n $data['owner'] = [\n 'uuid' => RequiresUUID::toNormal($row['owner_uuid']),\n 'name' => $row['owner_name'],\n 'photoUrl' => $row['owner_photo'] === null\n ? null\n : client_cdn($row['owner_photo'], $team),\n 'id' => $row['owner_id'],\n 'job' => $row['owner_job'],\n ];\n }\n\n $result[(int) $row['opp_id']] = $data;\n }\n\n return $this->hydrateStages($result);\n }\n\n private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder\n {\n $qb = clone $queryBuilder;\n $qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');\n\n $qb\n ->select(...[\n 'opp.id as opp_id',\n 'opp.uuid',\n 'opp.name',\n 'opp.value',\n 'opp.currency_code',\n 'opp.close_date',\n 'opp.remotely_created_at',\n 'opp.is_closed',\n 'opp.is_won',\n ])\n ->addSelect(...[\n 'usr.uuid as owner_uuid',\n 'usr.name AS owner_name',\n 'usr.photo_path as owner_photo',\n 'usr.id AS owner_id',\n 'jt.name as owner_job',\n ])\n ->addSelect('opp.stage_id', 'opp.stage_updated_at')\n ->addSelect(...[\n 'acc.name AS acc_name',\n 'acc.is_internal as acc_is_internal',\n 'opp.stage_updated_at',\n 'acc.crm_provider_id AS acc_provider_id',\n 'opp.crm_provider_id AS opp_provider_id',\n ])\n ->addSelect('rt.business_process_id AS pipeline_id')\n\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'));\n\n return $qb;\n }\n\n /**\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws SocialAccountTokenInvalidException\n */\n private function getCrmService(Team $team): ServiceInterface\n {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n $crmService->setConfiguration($team->crm);\n if ($crmService instanceof UrlGeneratorInterface) {\n $crmService->setCrmUrlGenerator($team->crm);\n }\n\n return $crmService;\n }\n\n /**\n *\n * @return Generator<DealData>\n */\n public function getForecastData(DealsFilter $filter): Generator\n {\n $opportunities = DB::query()\n ->select([\n 'o.value',\n 'o.close_date',\n 'o.currency_code',\n 'o.is_won',\n 'o.is_closed',\n 'o.probability',\n 'o.forecast_category',\n ])\n ->from('opportunities', 'o')\n ->join('users', 'users.id', '=', 'o.user_id')\n ->join('groups', 'groups.id', '=', 'users.group_id')\n ->where('users.team_id', $filter->getTeam()->getId())\n ->where('o.close_date', '>=', $filter->getStartDate())\n ->where('o.close_date', '<=', $filter->getEndDate())\n ->where('o.currency_code', $filter->getCurrency())\n ->where('o.deleted_at', '=', null)\n ;\n\n $userUuidList = $filter->getUserUuidList();\n if (! empty($userUuidList)) {\n $userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);\n\n $opportunities->whereIn('users.uuid', $userUuidList);\n }\n\n $groupUuidList = $filter->getGroupUuidList();\n if (! empty($groupUuidList)) {\n $groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);\n\n $opportunities->whereIn('groups.uuid', $groupUuidList);\n }\n\n foreach ($opportunities->cursor() as $row) {\n yield new DealData(\n (float) $row->value,\n $row->close_date,\n ! empty($row->is_won),\n ! empty($row->is_closed),\n $row->probability ?: 0,\n $row->forecast_category ?: '',\n );\n }\n }\n\n public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection\n {\n return $user->subscriptionSets()\n ->where(static function (Eloquent\\Builder $query): void {\n $query\n ->whereNull('expired_at')\n ->orWhere('expired_at', '>=', now());\n })\n ->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n $join\n ->where('followable_type', Models\\Activity\\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)\n ->whereIn('followable_id', $opportunityIds);\n })\n ->pluck('followable_id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.24335106,"top":0.047885075,"width":0.024268618,"height":0.024740623},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-2090548142966872323
|
-4167466464577769868
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
17
7
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active Ask Jiminny reports whose expiry date has passed.
*
* @return Collection<AutomatedReport>
*/
public function getExpiredActiveAskJiminnyReports(): Collection
{
return AutomatedReport::where('status', true)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereNotNull('expires_at')
->where('expires_at', '<', now()->toDateString())
->get();
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('report_id', $report->getId())
->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])
->latest()
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
public function countAllUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Restrict a query on the automated_reports table to reports the given user is allowed to see.
*
* Matches the customer-facing audience:
* - explicit user recipients (recipients.users)
* - members of any of the report's groups (Ask Jiminny reports)
*/
private function applyUserAccessScope(Builder $query, User $user): void
{
$userId = $user->getId();
$groupId = $user->getGroupId();
$query
->where('automated_reports.team_id', $user->getTeamId())
->where(function (Builder $q) use ($userId, $groupId): void {
$q->whereJsonContains('automated_reports.recipients->users', $userId);
if ($groupId !== null) {
$q->orWhere(function (Builder $sub) use ($groupId): void {
$sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereJsonContains('automated_reports.groups', $groupId);
});
}
});
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
36
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\DealInsights;
use Doctrine\DBAL\Connection;
use Generator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Jiminny\Component\DealInsights\Forecast\DealData;
use Jiminny\Component\DealInsights\Forecast\DealsFilter;
use Jiminny\Component\DealInsights\QueryBuilder\QueryBuilder;
use Jiminny\Component\DealInsights\QueryBuilder\Visitor\QueryBuilderVisitorInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Services\Crm\IntegrationApp\DTO\Utils\UrlGeneratorInterface;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class DealsRepository implements DealsRepositoryInterface
{
private Connection $connection;
private ProviderRegistry $providerRegistry;
/**
* @var QueryBuilderVisitorInterface[]
*/
private array $visitors = [];
/**
* @param QueryBuilderVisitorInterface[] $visitors
*/
public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])
{
$this->connection = $connection;
$this->providerRegistry = $crmProviderRegistry;
foreach ($visitors as $visitor) {
$this->visitors[$visitor->getIdentifier()] = $visitor;
}
}
public function getDeals(CriteriaInterface $criteria): array
{
$context = $criteria->getContext();
$team = $context->getTeam();
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$this->visit($qb, $criteria);
return $this->execute($team, $crmService, $qb);
}
public function getDeal(Team $team, int $id): array
{
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$qb->andWhere('opp.id = :id')->setParameter('id', $id);
return $this->execute($team, $crmService, $qb);
}
public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])
{
$qb = new QueryBuilder($this->connection);
$qb
->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')
->from('crm_fields', 'f')
->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')
->where('f.crm_configuration_id = :crm')
->andWhere('f.object_type = :type')
->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')
->orderBy('fd.object_id', 'ASC')
->addOrderBy('fd.updated_at', 'ASC')
->setParameter('type', Field::OBJECT_OPPORTUNITY)
->setParameter('crm', $crmId)
;
if (! empty($crmFields)) {
$fields = array_map(fn ($value): string => '"' . $value . '"', $crmFields);
$qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');
}
return $qb->executeQuery()->fetchAllAssociative();
}
public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAssociative();
}
public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('COALESCE(opp.currency_code, "' . $defaultCurrency . '") AS currency')
->addSelect('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
->groupBy('currency')
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAllAssociative();
}
public function getDealActivities(CriteriaInterface $criteria): array
{
$qb = Activity::with(['participants', 'user'])
->where('opportunity_id', $criteria->getOpportunityId())
->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())
->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())
->orderBy($criteria->getSortBy(), $criteria->getSortDirection())
;
// Should we filter activities by criteria? It's intended to filter deals.
return $qb->get()->all();
}
public function getStages(CriteriaInterface $criteria): array
{
$qb = new QueryBuilder($this->connection);
$qb
->select('id', 'label', 'sequence')
->from('stages', 's')
->where('crm_configuration_id = :crm_configuration_id')
->andWhere('type = :type')
->orderBy('sequence', 'ASC')
->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())
->setParameter('type', Stage::TYPE_OPPORTUNITY);
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$result[$row['id']] = [
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
public function getConfigurationStages(Configuration $configuration): Collection
{
return $configuration
->stages()
->where('type', Stage::TYPE_OPPORTUNITY)
->get();
}
public function getPipelineData(Configuration $crm): array
{
$qb = new QueryBuilder($this->connection);
$provider = $crm->provider;
$qb
->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')
->from('stages', 's')
->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')
->where('s.crm_configuration_id = :crm_configuration_id')
->andWhere('s.type = :type')
->orderBy('bps.business_process_id', 'ASC')
->addOrderBy('s.sequence', 'ASC')
->setParameter('crm_configuration_id', $crm->id)
->setParameter('type', Stage::TYPE_OPPORTUNITY)
;
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];
$result[$row['pipeline_id']][] = [
'value' => $value,
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
private function createQueryBuilder(string $realm): QueryBuilder
{
return (new QueryBuilder($this->connection))
->setRealm($realm)
->from('opportunities', 'opp')
->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')
->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')
->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')
;
}
/**
* Applies all applicable visitors and returns the IDs of the executed ones
*
* @return string[]
*/
private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array
{
$queryVisitors = [];
foreach ($this->visitors as $visitor) {
if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {
$visitor->visit($queryBuilder, $criteria);
$queryVisitors[] = $visitor->getIdentifier();
}
}
return $queryVisitors;
}
private function hydrateStages(array $deals): array
{
foreach ($this->fetchStages(array_keys($deals)) as $stage) {
$oppId = (int) $stage['opportunity_id'];
if (! isset($deals[$oppId])) {
continue; // or throw??!
}
$deals[$oppId]['stages'][] = [
'id' => $stage['stage_id'],
'name' => $stage['label'],
'enteredAt' => $stage['created_at'],
];
}
return $deals;
}
/**
* @param int[] $dealIds
*/
private function fetchStages(array $dealIds): array
{
if (empty($dealIds)) {
return [];
}
$qb = new QueryBuilder($this->connection);
$qb
->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')
->from('opportunity_stages', 'os')
->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')
->where($qb->expr()->in('os.opportunity_id', $dealIds))
->orderBy('os.opportunity_id', 'ASC')
->addOrderBy('s.created_at', 'ASC')
;
return $qb->executeQuery()->fetchAllAssociative();
}
private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array
{
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$data = [
'uuid' => RequiresUUID::toNormal($row['uuid']),
'name' => $row['name'],
'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),
'account' => [
'name' => $row['acc_name'],
'url' => $crmService->generateProviderUrl(
providerId: $row['acc_provider_id'],
objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'
),
],
'owner' => null,
'rawValue' => [
'amount' => (float) $row['value'],
'currency' => $row['currency_code'],
],
'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),
'openDate' => $row['remotely_created_at'] ?? null,
'closeDate' => $row['close_date'] ?? null,
'stages' => [],
'currentPipelineId' => $row['pipeline_id'],
'currentStage' => [
'id' => $row['stage_id'],
'enteredAt' => $row['stage_updated_at'],
],
'currentStageUpdatedAt' => $row['stage_updated_at'],
'isClosed' => (bool) $row['is_closed'],
'isWon' => (bool) $row['is_won'],
];
if (isset($row['owner_uuid'])) {
$data['owner'] = [
'uuid' => RequiresUUID::toNormal($row['owner_uuid']),
'name' => $row['owner_name'],
'photoUrl' => $row['owner_photo'] === null
? null
: client_cdn($row['owner_photo'], $team),
'id' => $row['owner_id'],
'job' => $row['owner_job'],
];
}
$result[(int) $row['opp_id']] = $data;
}
return $this->hydrateStages($result);
}
private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder
{
$qb = clone $queryBuilder;
$qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');
$qb
->select(...[
'opp.id as opp_id',
'opp.uuid',
'opp.name',
'opp.value',
'opp.currency_code',
'opp.close_date',
'opp.remotely_created_at',
'opp.is_closed',
'opp.is_won',
])
->addSelect(...[
'usr.uuid as owner_uuid',
'usr.name AS owner_name',
'usr.photo_path as owner_photo',
'usr.id AS owner_id',
'jt.name as owner_job',
])
->addSelect('opp.stage_id', 'opp.stage_updated_at')
->addSelect(...[
'acc.name AS acc_name',
'acc.is_internal as acc_is_internal',
'opp.stage_updated_at',
'acc.crm_provider_id AS acc_provider_id',
'opp.crm_provider_id AS opp_provider_id',
])
->addSelect('rt.business_process_id AS pipeline_id')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'));
return $qb;
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws SocialAccountTokenInvalidException
*/
private function getCrmService(Team $team): ServiceInterface
{
$crmService = $this->providerRegistry->get($team->crm->provider);
$crmService->setConfiguration($team->crm);
if ($crmService instanceof UrlGeneratorInterface) {
$crmService->setCrmUrlGenerator($team->crm);
}
return $crmService;
}
/**
*
* @return Generator<DealData>
*/
public function getForecastData(DealsFilter $filter): Generator
{
$opportunities = DB::query()
->select([
'o.value',
'o.close_date',
'o.currency_code',
'o.is_won',
'o.is_closed',
'o.probability',
'o.forecast_category',
])
->from('opportunities', 'o')
->join('users', 'users.id', '=', 'o.user_id')
->join('groups', 'groups.id', '=', 'users.group_id')
->where('users.team_id', $filter->getTeam()->getId())
->where('o.close_date', '>=', $filter->getStartDate())
->where('o.close_date', '<=', $filter->getEndDate())
->where('o.currency_code', $filter->getCurrency())
->where('o.deleted_at', '=', null)
;
$userUuidList = $filter->getUserUuidList();
if (! empty($userUuidList)) {
$userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);
$opportunities->whereIn('users.uuid', $userUuidList);
}
$groupUuidList = $filter->getGroupUuidList();
if (! empty($groupUuidList)) {
$groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);
$opportunities->whereIn('groups.uuid', $groupUuidList);
}
foreach ($opportunities->cursor() as $row) {
yield new DealData(
(float) $row->value,
$row->close_date,
! empty($row->is_won),
! empty($row->is_closed),
$row->probability ?: 0,
$row->forecast_category ?: '',
);
}
}
public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection
{
return $user->subscriptionSets()
->where(static function (Eloquent\Builder $query): void {
$query
->whereNull('expired_at')
->orWhere('expired_at', '>=', now());
})
->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {
$join
->on('subscription_set_id', '=', 'activity_subscription_sets.id');
$join
->where('followable_type', Models\Activity\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)
->whereIn('followable_id', $opportunityIds);
})
->pluck('followable_id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
|
73390
|
NULL
|
0
|
2026-04-23T07:31:11.194634+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776929471194_m1.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsRepository.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
17
7
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active Ask Jiminny reports whose expiry date has passed.
*
* @return Collection<AutomatedReport>
*/
public function getExpiredActiveAskJiminnyReports(): Collection
{
return AutomatedReport::where('status', true)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereNotNull('expires_at')
->where('expires_at', '<', now()->toDateString())
->get();
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('report_id', $report->getId())
->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])
->latest()
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
public function countAllUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Restrict a query on the automated_reports table to reports the given user is allowed to see.
*
* Matches the customer-facing audience:
* - explicit user recipients (recipients.users)
* - members of any of the report's groups (Ask Jiminny reports)
*/
private function applyUserAccessScope(Builder $query, User $user): void
{
$userId = $user->getId();
$groupId = $user->getGroupId();
$query
->where('automated_reports.team_id', $user->getTeamId())
->where(function (Builder $q) use ($userId, $groupId): void {
$q->whereJsonContains('automated_reports.recipients->users', $userId);
if ($groupId !== null) {
$q->orWhere(function (Builder $sub) use ($groupId): void {
$sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereJsonContains('automated_reports.groups', $groupId);
});
}
});
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
36
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\DealInsights;
use Doctrine\DBAL\Connection;
use Generator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Jiminny\Component\DealInsights\Forecast\DealData;
use Jiminny\Component\DealInsights\Forecast\DealsFilter;
use Jiminny\Component\DealInsights\QueryBuilder\QueryBuilder;
use Jiminny\Component\DealInsights\QueryBuilder\Visitor\QueryBuilderVisitorInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Services\Crm\IntegrationApp\DTO\Utils\UrlGeneratorInterface;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class DealsRepository implements DealsRepositoryInterface
{
private Connection $connection;
private ProviderRegistry $providerRegistry;
/**
* @var QueryBuilderVisitorInterface[]
*/
private array $visitors = [];
/**
* @param QueryBuilderVisitorInterface[] $visitors
*/
public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])
{
$this->connection = $connection;
$this->providerRegistry = $crmProviderRegistry;
foreach ($visitors as $visitor) {
$this->visitors[$visitor->getIdentifier()] = $visitor;
}
}
public function getDeals(CriteriaInterface $criteria): array
{
$context = $criteria->getContext();
$team = $context->getTeam();
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$this->visit($qb, $criteria);
return $this->execute($team, $crmService, $qb);
}
public function getDeal(Team $team, int $id): array
{
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$qb->andWhere('opp.id = :id')->setParameter('id', $id);
return $this->execute($team, $crmService, $qb);
}
public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])
{
$qb = new QueryBuilder($this->connection);
$qb
->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')
->from('crm_fields', 'f')
->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')
->where('f.crm_configuration_id = :crm')
->andWhere('f.object_type = :type')
->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')
->orderBy('fd.object_id', 'ASC')
->addOrderBy('fd.updated_at', 'ASC')
->setParameter('type', Field::OBJECT_OPPORTUNITY)
->setParameter('crm', $crmId)
;
if (! empty($crmFields)) {
$fields = array_map(fn ($value): string => '"' . $value . '"', $crmFields);
$qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');
}
return $qb->executeQuery()->fetchAllAssociative();
}
public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAssociative();
}
public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('COALESCE(opp.currency_code, "' . $defaultCurrency . '") AS currency')
->addSelect('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
->groupBy('currency')
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAllAssociative();
}
public function getDealActivities(CriteriaInterface $criteria): array
{
$qb = Activity::with(['participants', 'user'])
->where('opportunity_id', $criteria->getOpportunityId())
->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())
->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())
->orderBy($criteria->getSortBy(), $criteria->getSortDirection())
;
// Should we filter activities by criteria? It's intended to filter deals.
return $qb->get()->all();
}
public function getStages(CriteriaInterface $criteria): array
{
$qb = new QueryBuilder($this->connection);
$qb
->select('id', 'label', 'sequence')
->from('stages', 's')
->where('crm_configuration_id = :crm_configuration_id')
->andWhere('type = :type')
->orderBy('sequence', 'ASC')
->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())
->setParameter('type', Stage::TYPE_OPPORTUNITY);
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$result[$row['id']] = [
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
public function getConfigurationStages(Configuration $configuration): Collection
{
return $configuration
->stages()
->where('type', Stage::TYPE_OPPORTUNITY)
->get();
}
public function getPipelineData(Configuration $crm): array
{
$qb = new QueryBuilder($this->connection);
$provider = $crm->provider;
$qb
->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')
->from('stages', 's')
->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')
->where('s.crm_configuration_id = :crm_configuration_id')
->andWhere('s.type = :type')
->orderBy('bps.business_process_id', 'ASC')
->addOrderBy('s.sequence', 'ASC')
->setParameter('crm_configuration_id', $crm->id)
->setParameter('type', Stage::TYPE_OPPORTUNITY)
;
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];
$result[$row['pipeline_id']][] = [
'value' => $value,
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
private function createQueryBuilder(string $realm): QueryBuilder
{
return (new QueryBuilder($this->connection))
->setRealm($realm)
->from('opportunities', 'opp')
->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')
->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')
->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')
;
}
/**
* Applies all applicable visitors and returns the IDs of the executed ones
*
* @return string[]
*/
private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array
{
$queryVisitors = [];
foreach ($this->visitors as $visitor) {
if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {
$visitor->visit($queryBuilder, $criteria);
$queryVisitors[] = $visitor->getIdentifier();
}
}
return $queryVisitors;
}
private function hydrateStages(array $deals): array
{
foreach ($this->fetchStages(array_keys($deals)) as $stage) {
$oppId = (int) $stage['opportunity_id'];
if (! isset($deals[$oppId])) {
continue; // or throw??!
}
$deals[$oppId]['stages'][] = [
'id' => $stage['stage_id'],
'name' => $stage['label'],
'enteredAt' => $stage['created_at'],
];
}
return $deals;
}
/**
* @param int[] $dealIds
*/
private function fetchStages(array $dealIds): array
{
if (empty($dealIds)) {
return [];
}
$qb = new QueryBuilder($this->connection);
$qb
->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')
->from('opportunity_stages', 'os')
->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')
->where($qb->expr()->in('os.opportunity_id', $dealIds))
->orderBy('os.opportunity_id', 'ASC')
->addOrderBy('s.created_at', 'ASC')
;
return $qb->executeQuery()->fetchAllAssociative();
}
private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array
{
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$data = [
'uuid' => RequiresUUID::toNormal($row['uuid']),
'name' => $row['name'],
'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),
'account' => [
'name' => $row['acc_name'],
'url' => $crmService->generateProviderUrl(
providerId: $row['acc_provider_id'],
objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'
),
],
'owner' => null,
'rawValue' => [
'amount' => (float) $row['value'],
'currency' => $row['currency_code'],
],
'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),
'openDate' => $row['remotely_created_at'] ?? null,
'closeDate' => $row['close_date'] ?? null,
'stages' => [],
'currentPipelineId' => $row['pipeline_id'],
'currentStage' => [
'id' => $row['stage_id'],
'enteredAt' => $row['stage_updated_at'],
],
'currentStageUpdatedAt' => $row['stage_updated_at'],
'isClosed' => (bool) $row['is_closed'],
'isWon' => (bool) $row['is_won'],
];
if (isset($row['owner_uuid'])) {
$data['owner'] = [
'uuid' => RequiresUUID::toNormal($row['owner_uuid']),
'name' => $row['owner_name'],
'photoUrl' => $row['owner_photo'] === null
? null
: client_cdn($row['owner_photo'], $team),
'id' => $row['owner_id'],
'job' => $row['owner_job'],
];
}
$result[(int) $row['opp_id']] = $data;
}
return $this->hydrateStages($result);
}
private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder
{
$qb = clone $queryBuilder;
$qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');
$qb
->select(...[
'opp.id as opp_id',
'opp.uuid',
'opp.name',
'opp.value',
'opp.currency_code',
'opp.close_date',
'opp.remotely_created_at',
'opp.is_closed',
'opp.is_won',
])
->addSelect(...[
'usr.uuid as owner_uuid',
'usr.name AS owner_name',
'usr.photo_path as owner_photo',
'usr.id AS owner_id',
'jt.name as owner_job',
])
->addSelect('opp.stage_id', 'opp.stage_updated_at')
->addSelect(...[
'acc.name AS acc_name',
'acc.is_internal as acc_is_internal',
'opp.stage_updated_at',
'acc.crm_provider_id AS acc_provider_id',
'opp.crm_provider_id AS opp_provider_id',
])
->addSelect('rt.business_process_id AS pipeline_id')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'));
return $qb;
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws SocialAccountTokenInvalidException
*/
private function getCrmService(Team $team): ServiceInterface
{
$crmService = $this->providerRegistry->get($team->crm->provider);
$crmService->setConfiguration($team->crm);
if ($crmService instanceof UrlGeneratorInterface) {
$crmService->setCrmUrlGenerator($team->crm);
}
return $crmService;
}
/**
*
* @return Generator<DealData>
*/
public function getForecastData(DealsFilter $filter): Generator
{
$opportunities = DB::query()
->select([
'o.value',
'o.close_date',
'o.currency_code',
'o.is_won',
'o.is_closed',
'o.probability',
'o.forecast_category',
])
->from('opportunities', 'o')
->join('users', 'users.id', '=', 'o.user_id')
->join('groups', 'groups.id', '=', 'users.group_id')
->where('users.team_id', $filter->getTeam()->getId())
->where('o.close_date', '>=', $filter->getStartDate())
->where('o.close_date', '<=', $filter->getEndDate())
->where('o.currency_code', $filter->getCurrency())
->where('o.deleted_at', '=', null)
;
$userUuidList = $filter->getUserUuidList();
if (! empty($userUuidList)) {
$userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);
$opportunities->whereIn('users.uuid', $userUuidList);
}
$groupUuidList = $filter->getGroupUuidList();
if (! empty($groupUuidList)) {
$groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);
$opportunities->whereIn('groups.uuid', $groupUuidList);
}
foreach ($opportunities->cursor() as $row) {
yield new DealData(
(float) $row->value,
$row->close_date,
! empty($row->is_won),
! empty($row->is_closed),
$row->probability ?: 0,
$row->forecast_category ?: '',
);
}
}
public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection
{
return $user->subscriptionSets()
->where(static function (Eloquent\Builder $query): void {
$query
->whereNull('expired_at')
->orWhere('expired_at', '>=', now());
})
->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {
$join
->on('subscription_set_id', '=', 'activity_subscription_sets.id');
$join
->where('followable_type', Models\Activity\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)
->whereIn('followable_id', $opportunityIds);
})
->pluck('followable_id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20157-AJ-report-not-send-notification, menu","depth":5,"help_text":"Git Branch: JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"RequestGenerateAskJiminnyReportJobTest","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"17","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"7","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active Ask Jiminny reports whose expiry date has passed.\n *\n * @return Collection<AutomatedReport>\n */\n public function getExpiredActiveAskJiminnyReports(): Collection\n {\n return AutomatedReport::where('status', true)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereNotNull('expires_at')\n ->where('expires_at', '<', now()->toDateString())\n ->get();\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('report_id', $report->getId())\n ->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])\n ->latest()\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n \n public function countAllUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Restrict a query on the automated_reports table to reports the given user is allowed to see.\n *\n * Matches the customer-facing audience:\n * - explicit user recipients (recipients.users)\n * - members of any of the report's groups (Ask Jiminny reports)\n */\n private function applyUserAccessScope(Builder $query, User $user): void\n {\n $userId = $user->getId();\n $groupId = $user->getGroupId();\n\n $query\n ->where('automated_reports.team_id', $user->getTeamId())\n ->where(function (Builder $q) use ($userId, $groupId): void {\n $q->whereJsonContains('automated_reports.recipients->users', $userId);\n\n if ($groupId !== null) {\n $q->orWhere(function (Builder $sub) use ($groupId): void {\n $sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereJsonContains('automated_reports.groups', $groupId);\n });\n }\n });\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active Ask Jiminny reports whose expiry date has passed.\n *\n * @return Collection<AutomatedReport>\n */\n public function getExpiredActiveAskJiminnyReports(): Collection\n {\n return AutomatedReport::where('status', true)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereNotNull('expires_at')\n ->where('expires_at', '<', now()->toDateString())\n ->get();\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('report_id', $report->getId())\n ->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])\n ->latest()\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n \n public function countAllUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Restrict a query on the automated_reports table to reports the given user is allowed to see.\n *\n * Matches the customer-facing audience:\n * - explicit user recipients (recipients.users)\n * - members of any of the report's groups (Ask Jiminny reports)\n */\n private function applyUserAccessScope(Builder $query, User $user): void\n {\n $userId = $user->getId();\n $groupId = $user->getGroupId();\n\n $query\n ->where('automated_reports.team_id', $user->getTeamId())\n ->where(function (Builder $q) use ($userId, $groupId): void {\n $q->whereJsonContains('automated_reports.recipients->users', $userId);\n\n if ($groupId !== null) {\n $q->orWhere(function (Builder $sub) use ($groupId): void {\n $sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereJsonContains('automated_reports.groups', $groupId);\n });\n }\n });\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"36","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\DealInsights;\n\nuse Doctrine\\DBAL\\Connection;\nuse Generator;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealData;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealsFilter;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\QueryBuilder;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\Visitor\\QueryBuilderVisitorInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Services\\Crm\\IntegrationApp\\DTO\\Utils\\UrlGeneratorInterface;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Eloquent;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass DealsRepository implements DealsRepositoryInterface\n{\n private Connection $connection;\n\n private ProviderRegistry $providerRegistry;\n\n /**\n * @var QueryBuilderVisitorInterface[]\n */\n private array $visitors = [];\n\n /**\n * @param QueryBuilderVisitorInterface[] $visitors\n */\n public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])\n {\n $this->connection = $connection;\n $this->providerRegistry = $crmProviderRegistry;\n\n foreach ($visitors as $visitor) {\n $this->visitors[$visitor->getIdentifier()] = $visitor;\n }\n }\n\n public function getDeals(CriteriaInterface $criteria): array\n {\n $context = $criteria->getContext();\n $team = $context->getTeam();\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n\n $this->visit($qb, $criteria);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getDeal(Team $team, int $id): array\n {\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n $qb->andWhere('opp.id = :id')->setParameter('id', $id);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')\n ->from('crm_fields', 'f')\n ->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')\n ->where('f.crm_configuration_id = :crm')\n ->andWhere('f.object_type = :type')\n ->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')\n ->orderBy('fd.object_id', 'ASC')\n ->addOrderBy('fd.updated_at', 'ASC')\n\n ->setParameter('type', Field::OBJECT_OPPORTUNITY)\n ->setParameter('crm', $crmId)\n ;\n\n if (! empty($crmFields)) {\n $fields = array_map(fn ($value): string => '\"' . $value . '\"', $crmFields);\n $qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');\n }\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAssociative();\n }\n\n public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('COALESCE(opp.currency_code, \"' . $defaultCurrency . '\") AS currency')\n ->addSelect('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ->groupBy('currency')\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getDealActivities(CriteriaInterface $criteria): array\n {\n $qb = Activity::with(['participants', 'user'])\n ->where('opportunity_id', $criteria->getOpportunityId())\n ->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())\n ->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())\n ->orderBy($criteria->getSortBy(), $criteria->getSortDirection())\n ;\n\n // Should we filter activities by criteria? It's intended to filter deals.\n\n return $qb->get()->all();\n }\n\n public function getStages(CriteriaInterface $criteria): array\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('id', 'label', 'sequence')\n ->from('stages', 's')\n ->where('crm_configuration_id = :crm_configuration_id')\n ->andWhere('type = :type')\n ->orderBy('sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())\n ->setParameter('type', Stage::TYPE_OPPORTUNITY);\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $result[$row['id']] = [\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n public function getConfigurationStages(Configuration $configuration): Collection\n {\n return $configuration\n ->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get();\n }\n\n public function getPipelineData(Configuration $crm): array\n {\n $qb = new QueryBuilder($this->connection);\n $provider = $crm->provider;\n\n $qb\n ->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')\n ->from('stages', 's')\n ->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')\n ->where('s.crm_configuration_id = :crm_configuration_id')\n ->andWhere('s.type = :type')\n ->orderBy('bps.business_process_id', 'ASC')\n ->addOrderBy('s.sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $crm->id)\n ->setParameter('type', Stage::TYPE_OPPORTUNITY)\n ;\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];\n $result[$row['pipeline_id']][] = [\n 'value' => $value,\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n private function createQueryBuilder(string $realm): QueryBuilder\n {\n return (new QueryBuilder($this->connection))\n ->setRealm($realm)\n ->from('opportunities', 'opp')\n ->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')\n ->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')\n ->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')\n ;\n }\n\n /**\n * Applies all applicable visitors and returns the IDs of the executed ones\n *\n * @return string[]\n */\n private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array\n {\n $queryVisitors = [];\n\n foreach ($this->visitors as $visitor) {\n if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {\n $visitor->visit($queryBuilder, $criteria);\n\n $queryVisitors[] = $visitor->getIdentifier();\n }\n }\n\n return $queryVisitors;\n }\n\n private function hydrateStages(array $deals): array\n {\n foreach ($this->fetchStages(array_keys($deals)) as $stage) {\n $oppId = (int) $stage['opportunity_id'];\n\n if (! isset($deals[$oppId])) {\n continue; // or throw??!\n }\n\n $deals[$oppId]['stages'][] = [\n 'id' => $stage['stage_id'],\n 'name' => $stage['label'],\n 'enteredAt' => $stage['created_at'],\n ];\n }\n\n return $deals;\n }\n\n /**\n * @param int[] $dealIds\n */\n private function fetchStages(array $dealIds): array\n {\n if (empty($dealIds)) {\n return [];\n }\n\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')\n ->from('opportunity_stages', 'os')\n ->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')\n ->where($qb->expr()->in('os.opportunity_id', $dealIds))\n ->orderBy('os.opportunity_id', 'ASC')\n ->addOrderBy('s.created_at', 'ASC')\n ;\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array\n {\n $result = [];\n\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $data = [\n 'uuid' => RequiresUUID::toNormal($row['uuid']),\n 'name' => $row['name'],\n 'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),\n 'account' => [\n 'name' => $row['acc_name'],\n 'url' => $crmService->generateProviderUrl(\n providerId: $row['acc_provider_id'],\n objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'\n ),\n ],\n 'owner' => null,\n 'rawValue' => [\n 'amount' => (float) $row['value'],\n 'currency' => $row['currency_code'],\n ],\n 'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),\n 'openDate' => $row['remotely_created_at'] ?? null,\n 'closeDate' => $row['close_date'] ?? null,\n 'stages' => [],\n 'currentPipelineId' => $row['pipeline_id'],\n 'currentStage' => [\n 'id' => $row['stage_id'],\n 'enteredAt' => $row['stage_updated_at'],\n ],\n 'currentStageUpdatedAt' => $row['stage_updated_at'],\n 'isClosed' => (bool) $row['is_closed'],\n 'isWon' => (bool) $row['is_won'],\n ];\n\n if (isset($row['owner_uuid'])) {\n $data['owner'] = [\n 'uuid' => RequiresUUID::toNormal($row['owner_uuid']),\n 'name' => $row['owner_name'],\n 'photoUrl' => $row['owner_photo'] === null\n ? null\n : client_cdn($row['owner_photo'], $team),\n 'id' => $row['owner_id'],\n 'job' => $row['owner_job'],\n ];\n }\n\n $result[(int) $row['opp_id']] = $data;\n }\n\n return $this->hydrateStages($result);\n }\n\n private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder\n {\n $qb = clone $queryBuilder;\n $qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');\n\n $qb\n ->select(...[\n 'opp.id as opp_id',\n 'opp.uuid',\n 'opp.name',\n 'opp.value',\n 'opp.currency_code',\n 'opp.close_date',\n 'opp.remotely_created_at',\n 'opp.is_closed',\n 'opp.is_won',\n ])\n ->addSelect(...[\n 'usr.uuid as owner_uuid',\n 'usr.name AS owner_name',\n 'usr.photo_path as owner_photo',\n 'usr.id AS owner_id',\n 'jt.name as owner_job',\n ])\n ->addSelect('opp.stage_id', 'opp.stage_updated_at')\n ->addSelect(...[\n 'acc.name AS acc_name',\n 'acc.is_internal as acc_is_internal',\n 'opp.stage_updated_at',\n 'acc.crm_provider_id AS acc_provider_id',\n 'opp.crm_provider_id AS opp_provider_id',\n ])\n ->addSelect('rt.business_process_id AS pipeline_id')\n\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'));\n\n return $qb;\n }\n\n /**\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws SocialAccountTokenInvalidException\n */\n private function getCrmService(Team $team): ServiceInterface\n {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n $crmService->setConfiguration($team->crm);\n if ($crmService instanceof UrlGeneratorInterface) {\n $crmService->setCrmUrlGenerator($team->crm);\n }\n\n return $crmService;\n }\n\n /**\n *\n * @return Generator<DealData>\n */\n public function getForecastData(DealsFilter $filter): Generator\n {\n $opportunities = DB::query()\n ->select([\n 'o.value',\n 'o.close_date',\n 'o.currency_code',\n 'o.is_won',\n 'o.is_closed',\n 'o.probability',\n 'o.forecast_category',\n ])\n ->from('opportunities', 'o')\n ->join('users', 'users.id', '=', 'o.user_id')\n ->join('groups', 'groups.id', '=', 'users.group_id')\n ->where('users.team_id', $filter->getTeam()->getId())\n ->where('o.close_date', '>=', $filter->getStartDate())\n ->where('o.close_date', '<=', $filter->getEndDate())\n ->where('o.currency_code', $filter->getCurrency())\n ->where('o.deleted_at', '=', null)\n ;\n\n $userUuidList = $filter->getUserUuidList();\n if (! empty($userUuidList)) {\n $userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);\n\n $opportunities->whereIn('users.uuid', $userUuidList);\n }\n\n $groupUuidList = $filter->getGroupUuidList();\n if (! empty($groupUuidList)) {\n $groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);\n\n $opportunities->whereIn('groups.uuid', $groupUuidList);\n }\n\n foreach ($opportunities->cursor() as $row) {\n yield new DealData(\n (float) $row->value,\n $row->close_date,\n ! empty($row->is_won),\n ! empty($row->is_closed),\n $row->probability ?: 0,\n $row->forecast_category ?: '',\n );\n }\n }\n\n public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection\n {\n return $user->subscriptionSets()\n ->where(static function (Eloquent\\Builder $query): void {\n $query\n ->whereNull('expired_at')\n ->orWhere('expired_at', '>=', now());\n })\n ->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n $join\n ->where('followable_type', Models\\Activity\\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)\n ->whereIn('followable_id', $opportunityIds);\n })\n ->pluck('followable_id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\DealInsights;\n\nuse Doctrine\\DBAL\\Connection;\nuse Generator;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealData;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealsFilter;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\QueryBuilder;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\Visitor\\QueryBuilderVisitorInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Services\\Crm\\IntegrationApp\\DTO\\Utils\\UrlGeneratorInterface;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Eloquent;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass DealsRepository implements DealsRepositoryInterface\n{\n private Connection $connection;\n\n private ProviderRegistry $providerRegistry;\n\n /**\n * @var QueryBuilderVisitorInterface[]\n */\n private array $visitors = [];\n\n /**\n * @param QueryBuilderVisitorInterface[] $visitors\n */\n public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])\n {\n $this->connection = $connection;\n $this->providerRegistry = $crmProviderRegistry;\n\n foreach ($visitors as $visitor) {\n $this->visitors[$visitor->getIdentifier()] = $visitor;\n }\n }\n\n public function getDeals(CriteriaInterface $criteria): array\n {\n $context = $criteria->getContext();\n $team = $context->getTeam();\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n\n $this->visit($qb, $criteria);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getDeal(Team $team, int $id): array\n {\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n $qb->andWhere('opp.id = :id')->setParameter('id', $id);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')\n ->from('crm_fields', 'f')\n ->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')\n ->where('f.crm_configuration_id = :crm')\n ->andWhere('f.object_type = :type')\n ->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')\n ->orderBy('fd.object_id', 'ASC')\n ->addOrderBy('fd.updated_at', 'ASC')\n\n ->setParameter('type', Field::OBJECT_OPPORTUNITY)\n ->setParameter('crm', $crmId)\n ;\n\n if (! empty($crmFields)) {\n $fields = array_map(fn ($value): string => '\"' . $value . '\"', $crmFields);\n $qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');\n }\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAssociative();\n }\n\n public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('COALESCE(opp.currency_code, \"' . $defaultCurrency . '\") AS currency')\n ->addSelect('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ->groupBy('currency')\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getDealActivities(CriteriaInterface $criteria): array\n {\n $qb = Activity::with(['participants', 'user'])\n ->where('opportunity_id', $criteria->getOpportunityId())\n ->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())\n ->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())\n ->orderBy($criteria->getSortBy(), $criteria->getSortDirection())\n ;\n\n // Should we filter activities by criteria? It's intended to filter deals.\n\n return $qb->get()->all();\n }\n\n public function getStages(CriteriaInterface $criteria): array\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('id', 'label', 'sequence')\n ->from('stages', 's')\n ->where('crm_configuration_id = :crm_configuration_id')\n ->andWhere('type = :type')\n ->orderBy('sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())\n ->setParameter('type', Stage::TYPE_OPPORTUNITY);\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $result[$row['id']] = [\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n public function getConfigurationStages(Configuration $configuration): Collection\n {\n return $configuration\n ->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get();\n }\n\n public function getPipelineData(Configuration $crm): array\n {\n $qb = new QueryBuilder($this->connection);\n $provider = $crm->provider;\n\n $qb\n ->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')\n ->from('stages', 's')\n ->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')\n ->where('s.crm_configuration_id = :crm_configuration_id')\n ->andWhere('s.type = :type')\n ->orderBy('bps.business_process_id', 'ASC')\n ->addOrderBy('s.sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $crm->id)\n ->setParameter('type', Stage::TYPE_OPPORTUNITY)\n ;\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];\n $result[$row['pipeline_id']][] = [\n 'value' => $value,\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n private function createQueryBuilder(string $realm): QueryBuilder\n {\n return (new QueryBuilder($this->connection))\n ->setRealm($realm)\n ->from('opportunities', 'opp')\n ->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')\n ->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')\n ->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')\n ;\n }\n\n /**\n * Applies all applicable visitors and returns the IDs of the executed ones\n *\n * @return string[]\n */\n private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array\n {\n $queryVisitors = [];\n\n foreach ($this->visitors as $visitor) {\n if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {\n $visitor->visit($queryBuilder, $criteria);\n\n $queryVisitors[] = $visitor->getIdentifier();\n }\n }\n\n return $queryVisitors;\n }\n\n private function hydrateStages(array $deals): array\n {\n foreach ($this->fetchStages(array_keys($deals)) as $stage) {\n $oppId = (int) $stage['opportunity_id'];\n\n if (! isset($deals[$oppId])) {\n continue; // or throw??!\n }\n\n $deals[$oppId]['stages'][] = [\n 'id' => $stage['stage_id'],\n 'name' => $stage['label'],\n 'enteredAt' => $stage['created_at'],\n ];\n }\n\n return $deals;\n }\n\n /**\n * @param int[] $dealIds\n */\n private function fetchStages(array $dealIds): array\n {\n if (empty($dealIds)) {\n return [];\n }\n\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')\n ->from('opportunity_stages', 'os')\n ->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')\n ->where($qb->expr()->in('os.opportunity_id', $dealIds))\n ->orderBy('os.opportunity_id', 'ASC')\n ->addOrderBy('s.created_at', 'ASC')\n ;\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array\n {\n $result = [];\n\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $data = [\n 'uuid' => RequiresUUID::toNormal($row['uuid']),\n 'name' => $row['name'],\n 'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),\n 'account' => [\n 'name' => $row['acc_name'],\n 'url' => $crmService->generateProviderUrl(\n providerId: $row['acc_provider_id'],\n objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'\n ),\n ],\n 'owner' => null,\n 'rawValue' => [\n 'amount' => (float) $row['value'],\n 'currency' => $row['currency_code'],\n ],\n 'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),\n 'openDate' => $row['remotely_created_at'] ?? null,\n 'closeDate' => $row['close_date'] ?? null,\n 'stages' => [],\n 'currentPipelineId' => $row['pipeline_id'],\n 'currentStage' => [\n 'id' => $row['stage_id'],\n 'enteredAt' => $row['stage_updated_at'],\n ],\n 'currentStageUpdatedAt' => $row['stage_updated_at'],\n 'isClosed' => (bool) $row['is_closed'],\n 'isWon' => (bool) $row['is_won'],\n ];\n\n if (isset($row['owner_uuid'])) {\n $data['owner'] = [\n 'uuid' => RequiresUUID::toNormal($row['owner_uuid']),\n 'name' => $row['owner_name'],\n 'photoUrl' => $row['owner_photo'] === null\n ? null\n : client_cdn($row['owner_photo'], $team),\n 'id' => $row['owner_id'],\n 'job' => $row['owner_job'],\n ];\n }\n\n $result[(int) $row['opp_id']] = $data;\n }\n\n return $this->hydrateStages($result);\n }\n\n private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder\n {\n $qb = clone $queryBuilder;\n $qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');\n\n $qb\n ->select(...[\n 'opp.id as opp_id',\n 'opp.uuid',\n 'opp.name',\n 'opp.value',\n 'opp.currency_code',\n 'opp.close_date',\n 'opp.remotely_created_at',\n 'opp.is_closed',\n 'opp.is_won',\n ])\n ->addSelect(...[\n 'usr.uuid as owner_uuid',\n 'usr.name AS owner_name',\n 'usr.photo_path as owner_photo',\n 'usr.id AS owner_id',\n 'jt.name as owner_job',\n ])\n ->addSelect('opp.stage_id', 'opp.stage_updated_at')\n ->addSelect(...[\n 'acc.name AS acc_name',\n 'acc.is_internal as acc_is_internal',\n 'opp.stage_updated_at',\n 'acc.crm_provider_id AS acc_provider_id',\n 'opp.crm_provider_id AS opp_provider_id',\n ])\n ->addSelect('rt.business_process_id AS pipeline_id')\n\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'));\n\n return $qb;\n }\n\n /**\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws SocialAccountTokenInvalidException\n */\n private function getCrmService(Team $team): ServiceInterface\n {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n $crmService->setConfiguration($team->crm);\n if ($crmService instanceof UrlGeneratorInterface) {\n $crmService->setCrmUrlGenerator($team->crm);\n }\n\n return $crmService;\n }\n\n /**\n *\n * @return Generator<DealData>\n */\n public function getForecastData(DealsFilter $filter): Generator\n {\n $opportunities = DB::query()\n ->select([\n 'o.value',\n 'o.close_date',\n 'o.currency_code',\n 'o.is_won',\n 'o.is_closed',\n 'o.probability',\n 'o.forecast_category',\n ])\n ->from('opportunities', 'o')\n ->join('users', 'users.id', '=', 'o.user_id')\n ->join('groups', 'groups.id', '=', 'users.group_id')\n ->where('users.team_id', $filter->getTeam()->getId())\n ->where('o.close_date', '>=', $filter->getStartDate())\n ->where('o.close_date', '<=', $filter->getEndDate())\n ->where('o.currency_code', $filter->getCurrency())\n ->where('o.deleted_at', '=', null)\n ;\n\n $userUuidList = $filter->getUserUuidList();\n if (! empty($userUuidList)) {\n $userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);\n\n $opportunities->whereIn('users.uuid', $userUuidList);\n }\n\n $groupUuidList = $filter->getGroupUuidList();\n if (! empty($groupUuidList)) {\n $groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);\n\n $opportunities->whereIn('groups.uuid', $groupUuidList);\n }\n\n foreach ($opportunities->cursor() as $row) {\n yield new DealData(\n (float) $row->value,\n $row->close_date,\n ! empty($row->is_won),\n ! empty($row->is_closed),\n $row->probability ?: 0,\n $row->forecast_category ?: '',\n );\n }\n }\n\n public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection\n {\n return $user->subscriptionSets()\n ->where(static function (Eloquent\\Builder $query): void {\n $query\n ->whereNull('expired_at')\n ->orWhere('expired_at', '>=', now());\n })\n ->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n $join\n ->where('followable_type', Models\\Activity\\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)\n ->whereIn('followable_id', $opportunityIds);\n })\n ->pluck('followable_id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-2090548142966872323
|
-4167466464577769868
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
17
7
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active Ask Jiminny reports whose expiry date has passed.
*
* @return Collection<AutomatedReport>
*/
public function getExpiredActiveAskJiminnyReports(): Collection
{
return AutomatedReport::where('status', true)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereNotNull('expires_at')
->where('expires_at', '<', now()->toDateString())
->get();
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('report_id', $report->getId())
->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])
->latest()
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
public function countAllUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Restrict a query on the automated_reports table to reports the given user is allowed to see.
*
* Matches the customer-facing audience:
* - explicit user recipients (recipients.users)
* - members of any of the report's groups (Ask Jiminny reports)
*/
private function applyUserAccessScope(Builder $query, User $user): void
{
$userId = $user->getId();
$groupId = $user->getGroupId();
$query
->where('automated_reports.team_id', $user->getTeamId())
->where(function (Builder $q) use ($userId, $groupId): void {
$q->whereJsonContains('automated_reports.recipients->users', $userId);
if ($groupId !== null) {
$q->orWhere(function (Builder $sub) use ($groupId): void {
$sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereJsonContains('automated_reports.groups', $groupId);
});
}
});
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
36
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\DealInsights;
use Doctrine\DBAL\Connection;
use Generator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Jiminny\Component\DealInsights\Forecast\DealData;
use Jiminny\Component\DealInsights\Forecast\DealsFilter;
use Jiminny\Component\DealInsights\QueryBuilder\QueryBuilder;
use Jiminny\Component\DealInsights\QueryBuilder\Visitor\QueryBuilderVisitorInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Services\Crm\IntegrationApp\DTO\Utils\UrlGeneratorInterface;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class DealsRepository implements DealsRepositoryInterface
{
private Connection $connection;
private ProviderRegistry $providerRegistry;
/**
* @var QueryBuilderVisitorInterface[]
*/
private array $visitors = [];
/**
* @param QueryBuilderVisitorInterface[] $visitors
*/
public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])
{
$this->connection = $connection;
$this->providerRegistry = $crmProviderRegistry;
foreach ($visitors as $visitor) {
$this->visitors[$visitor->getIdentifier()] = $visitor;
}
}
public function getDeals(CriteriaInterface $criteria): array
{
$context = $criteria->getContext();
$team = $context->getTeam();
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$this->visit($qb, $criteria);
return $this->execute($team, $crmService, $qb);
}
public function getDeal(Team $team, int $id): array
{
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$qb->andWhere('opp.id = :id')->setParameter('id', $id);
return $this->execute($team, $crmService, $qb);
}
public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])
{
$qb = new QueryBuilder($this->connection);
$qb
->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')
->from('crm_fields', 'f')
->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')
->where('f.crm_configuration_id = :crm')
->andWhere('f.object_type = :type')
->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')
->orderBy('fd.object_id', 'ASC')
->addOrderBy('fd.updated_at', 'ASC')
->setParameter('type', Field::OBJECT_OPPORTUNITY)
->setParameter('crm', $crmId)
;
if (! empty($crmFields)) {
$fields = array_map(fn ($value): string => '"' . $value . '"', $crmFields);
$qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');
}
return $qb->executeQuery()->fetchAllAssociative();
}
public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAssociative();
}
public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('COALESCE(opp.currency_code, "' . $defaultCurrency . '") AS currency')
->addSelect('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
->groupBy('currency')
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAllAssociative();
}
public function getDealActivities(CriteriaInterface $criteria): array
{
$qb = Activity::with(['participants', 'user'])
->where('opportunity_id', $criteria->getOpportunityId())
->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())
->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())
->orderBy($criteria->getSortBy(), $criteria->getSortDirection())
;
// Should we filter activities by criteria? It's intended to filter deals.
return $qb->get()->all();
}
public function getStages(CriteriaInterface $criteria): array
{
$qb = new QueryBuilder($this->connection);
$qb
->select('id', 'label', 'sequence')
->from('stages', 's')
->where('crm_configuration_id = :crm_configuration_id')
->andWhere('type = :type')
->orderBy('sequence', 'ASC')
->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())
->setParameter('type', Stage::TYPE_OPPORTUNITY);
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$result[$row['id']] = [
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
public function getConfigurationStages(Configuration $configuration): Collection
{
return $configuration
->stages()
->where('type', Stage::TYPE_OPPORTUNITY)
->get();
}
public function getPipelineData(Configuration $crm): array
{
$qb = new QueryBuilder($this->connection);
$provider = $crm->provider;
$qb
->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')
->from('stages', 's')
->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')
->where('s.crm_configuration_id = :crm_configuration_id')
->andWhere('s.type = :type')
->orderBy('bps.business_process_id', 'ASC')
->addOrderBy('s.sequence', 'ASC')
->setParameter('crm_configuration_id', $crm->id)
->setParameter('type', Stage::TYPE_OPPORTUNITY)
;
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];
$result[$row['pipeline_id']][] = [
'value' => $value,
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
private function createQueryBuilder(string $realm): QueryBuilder
{
return (new QueryBuilder($this->connection))
->setRealm($realm)
->from('opportunities', 'opp')
->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')
->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')
->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')
;
}
/**
* Applies all applicable visitors and returns the IDs of the executed ones
*
* @return string[]
*/
private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array
{
$queryVisitors = [];
foreach ($this->visitors as $visitor) {
if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {
$visitor->visit($queryBuilder, $criteria);
$queryVisitors[] = $visitor->getIdentifier();
}
}
return $queryVisitors;
}
private function hydrateStages(array $deals): array
{
foreach ($this->fetchStages(array_keys($deals)) as $stage) {
$oppId = (int) $stage['opportunity_id'];
if (! isset($deals[$oppId])) {
continue; // or throw??!
}
$deals[$oppId]['stages'][] = [
'id' => $stage['stage_id'],
'name' => $stage['label'],
'enteredAt' => $stage['created_at'],
];
}
return $deals;
}
/**
* @param int[] $dealIds
*/
private function fetchStages(array $dealIds): array
{
if (empty($dealIds)) {
return [];
}
$qb = new QueryBuilder($this->connection);
$qb
->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')
->from('opportunity_stages', 'os')
->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')
->where($qb->expr()->in('os.opportunity_id', $dealIds))
->orderBy('os.opportunity_id', 'ASC')
->addOrderBy('s.created_at', 'ASC')
;
return $qb->executeQuery()->fetchAllAssociative();
}
private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array
{
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$data = [
'uuid' => RequiresUUID::toNormal($row['uuid']),
'name' => $row['name'],
'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),
'account' => [
'name' => $row['acc_name'],
'url' => $crmService->generateProviderUrl(
providerId: $row['acc_provider_id'],
objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'
),
],
'owner' => null,
'rawValue' => [
'amount' => (float) $row['value'],
'currency' => $row['currency_code'],
],
'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),
'openDate' => $row['remotely_created_at'] ?? null,
'closeDate' => $row['close_date'] ?? null,
'stages' => [],
'currentPipelineId' => $row['pipeline_id'],
'currentStage' => [
'id' => $row['stage_id'],
'enteredAt' => $row['stage_updated_at'],
],
'currentStageUpdatedAt' => $row['stage_updated_at'],
'isClosed' => (bool) $row['is_closed'],
'isWon' => (bool) $row['is_won'],
];
if (isset($row['owner_uuid'])) {
$data['owner'] = [
'uuid' => RequiresUUID::toNormal($row['owner_uuid']),
'name' => $row['owner_name'],
'photoUrl' => $row['owner_photo'] === null
? null
: client_cdn($row['owner_photo'], $team),
'id' => $row['owner_id'],
'job' => $row['owner_job'],
];
}
$result[(int) $row['opp_id']] = $data;
}
return $this->hydrateStages($result);
}
private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder
{
$qb = clone $queryBuilder;
$qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');
$qb
->select(...[
'opp.id as opp_id',
'opp.uuid',
'opp.name',
'opp.value',
'opp.currency_code',
'opp.close_date',
'opp.remotely_created_at',
'opp.is_closed',
'opp.is_won',
])
->addSelect(...[
'usr.uuid as owner_uuid',
'usr.name AS owner_name',
'usr.photo_path as owner_photo',
'usr.id AS owner_id',
'jt.name as owner_job',
])
->addSelect('opp.stage_id', 'opp.stage_updated_at')
->addSelect(...[
'acc.name AS acc_name',
'acc.is_internal as acc_is_internal',
'opp.stage_updated_at',
'acc.crm_provider_id AS acc_provider_id',
'opp.crm_provider_id AS opp_provider_id',
])
->addSelect('rt.business_process_id AS pipeline_id')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'));
return $qb;
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws SocialAccountTokenInvalidException
*/
private function getCrmService(Team $team): ServiceInterface
{
$crmService = $this->providerRegistry->get($team->crm->provider);
$crmService->setConfiguration($team->crm);
if ($crmService instanceof UrlGeneratorInterface) {
$crmService->setCrmUrlGenerator($team->crm);
}
return $crmService;
}
/**
*
* @return Generator<DealData>
*/
public function getForecastData(DealsFilter $filter): Generator
{
$opportunities = DB::query()
->select([
'o.value',
'o.close_date',
'o.currency_code',
'o.is_won',
'o.is_closed',
'o.probability',
'o.forecast_category',
])
->from('opportunities', 'o')
->join('users', 'users.id', '=', 'o.user_id')
->join('groups', 'groups.id', '=', 'users.group_id')
->where('users.team_id', $filter->getTeam()->getId())
->where('o.close_date', '>=', $filter->getStartDate())
->where('o.close_date', '<=', $filter->getEndDate())
->where('o.currency_code', $filter->getCurrency())
->where('o.deleted_at', '=', null)
;
$userUuidList = $filter->getUserUuidList();
if (! empty($userUuidList)) {
$userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);
$opportunities->whereIn('users.uuid', $userUuidList);
}
$groupUuidList = $filter->getGroupUuidList();
if (! empty($groupUuidList)) {
$groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);
$opportunities->whereIn('groups.uuid', $groupUuidList);
}
foreach ($opportunities->cursor() as $row) {
yield new DealData(
(float) $row->value,
$row->close_date,
! empty($row->is_won),
! empty($row->is_closed),
$row->probability ?: 0,
$row->forecast_category ?: '',
);
}
}
public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection
{
return $user->subscriptionSets()
->where(static function (Eloquent\Builder $query): void {
$query
->whereNull('expired_at')
->orWhere('expired_at', '>=', now());
})
->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {
$join
->on('subscription_set_id', '=', 'activity_subscription_sets.id');
$join
->where('followable_type', Models\Activity\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)
->whereIn('followable_id', $opportunityIds);
})
->pluck('followable_id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
|
73361
|
NULL
|
0
|
2026-04-23T07:26:15.927515+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776929175927_m1.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsRepository.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
17
7
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active Ask Jiminny reports whose expiry date has passed.
*
* @return Collection<AutomatedReport>
*/
public function getExpiredActiveAskJiminnyReports(): Collection
{
return AutomatedReport::where('status', true)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereNotNull('expires_at')
->where('expires_at', '<', now()->toDateString())
->get();
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('report_id', $report->getId())
->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])
->latest()
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
public function countAllUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Restrict a query on the automated_reports table to reports the given user is allowed to see.
*
* Matches the customer-facing audience:
* - explicit user recipients (recipients.users)
* - members of any of the report's groups (Ask Jiminny reports)
*/
private function applyUserAccessScope(Builder $query, User $user): void
{
$userId = $user->getId();
$groupId = $user->getGroupId();
$query
->where('automated_reports.team_id', $user->getTeamId())
->where(function (Builder $q) use ($userId, $groupId): void {
$q->whereJsonContains('automated_reports.recipients->users', $userId);
if ($groupId !== null) {
$q->orWhere(function (Builder $sub) use ($groupId): void {
$sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereJsonContains('automated_reports.groups', $groupId);
});
}
});
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
36
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\DealInsights;
use Doctrine\DBAL\Connection;
use Generator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Jiminny\Component\DealInsights\Forecast\DealData;
use Jiminny\Component\DealInsights\Forecast\DealsFilter;
use Jiminny\Component\DealInsights\QueryBuilder\QueryBuilder;
use Jiminny\Component\DealInsights\QueryBuilder\Visitor\QueryBuilderVisitorInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Services\Crm\IntegrationApp\DTO\Utils\UrlGeneratorInterface;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class DealsRepository implements DealsRepositoryInterface
{
private Connection $connection;
private ProviderRegistry $providerRegistry;
/**
* @var QueryBuilderVisitorInterface[]
*/
private array $visitors = [];
/**
* @param QueryBuilderVisitorInterface[] $visitors
*/
public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])
{
$this->connection = $connection;
$this->providerRegistry = $crmProviderRegistry;
foreach ($visitors as $visitor) {
$this->visitors[$visitor->getIdentifier()] = $visitor;
}
}
public function getDeals(CriteriaInterface $criteria): array
{
$context = $criteria->getContext();
$team = $context->getTeam();
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$this->visit($qb, $criteria);
return $this->execute($team, $crmService, $qb);
}
public function getDeal(Team $team, int $id): array
{
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$qb->andWhere('opp.id = :id')->setParameter('id', $id);
return $this->execute($team, $crmService, $qb);
}
public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])
{
$qb = new QueryBuilder($this->connection);
$qb
->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')
->from('crm_fields', 'f')
->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')
->where('f.crm_configuration_id = :crm')
->andWhere('f.object_type = :type')
->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')
->orderBy('fd.object_id', 'ASC')
->addOrderBy('fd.updated_at', 'ASC')
->setParameter('type', Field::OBJECT_OPPORTUNITY)
->setParameter('crm', $crmId)
;
if (! empty($crmFields)) {
$fields = array_map(fn ($value): string => '"' . $value . '"', $crmFields);
$qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');
}
return $qb->executeQuery()->fetchAllAssociative();
}
public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAssociative();
}
public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('COALESCE(opp.currency_code, "' . $defaultCurrency . '") AS currency')
->addSelect('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
->groupBy('currency')
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAllAssociative();
}
public function getDealActivities(CriteriaInterface $criteria): array
{
$qb = Activity::with(['participants', 'user'])
->where('opportunity_id', $criteria->getOpportunityId())
->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())
->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())
->orderBy($criteria->getSortBy(), $criteria->getSortDirection())
;
// Should we filter activities by criteria? It's intended to filter deals.
return $qb->get()->all();
}
public function getStages(CriteriaInterface $criteria): array
{
$qb = new QueryBuilder($this->connection);
$qb
->select('id', 'label', 'sequence')
->from('stages', 's')
->where('crm_configuration_id = :crm_configuration_id')
->andWhere('type = :type')
->orderBy('sequence', 'ASC')
->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())
->setParameter('type', Stage::TYPE_OPPORTUNITY);
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$result[$row['id']] = [
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
public function getConfigurationStages(Configuration $configuration): Collection
{
return $configuration
->stages()
->where('type', Stage::TYPE_OPPORTUNITY)
->get();
}
public function getPipelineData(Configuration $crm): array
{
$qb = new QueryBuilder($this->connection);
$provider = $crm->provider;
$qb
->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')
->from('stages', 's')
->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')
->where('s.crm_configuration_id = :crm_configuration_id')
->andWhere('s.type = :type')
->orderBy('bps.business_process_id', 'ASC')
->addOrderBy('s.sequence', 'ASC')
->setParameter('crm_configuration_id', $crm->id)
->setParameter('type', Stage::TYPE_OPPORTUNITY)
;
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];
$result[$row['pipeline_id']][] = [
'value' => $value,
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
private function createQueryBuilder(string $realm): QueryBuilder
{
return (new QueryBuilder($this->connection))
->setRealm($realm)
->from('opportunities', 'opp')
->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')
->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')
->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')
;
}
/**
* Applies all applicable visitors and returns the IDs of the executed ones
*
* @return string[]
*/
private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array
{
$queryVisitors = [];
foreach ($this->visitors as $visitor) {
if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {
$visitor->visit($queryBuilder, $criteria);
$queryVisitors[] = $visitor->getIdentifier();
}
}
return $queryVisitors;
}
private function hydrateStages(array $deals): array
{
foreach ($this->fetchStages(array_keys($deals)) as $stage) {
$oppId = (int) $stage['opportunity_id'];
if (! isset($deals[$oppId])) {
continue; // or throw??!
}
$deals[$oppId]['stages'][] = [
'id' => $stage['stage_id'],
'name' => $stage['label'],
'enteredAt' => $stage['created_at'],
];
}
return $deals;
}
/**
* @param int[] $dealIds
*/
private function fetchStages(array $dealIds): array
{
if (empty($dealIds)) {
return [];
}
$qb = new QueryBuilder($this->connection);
$qb
->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')
->from('opportunity_stages', 'os')
->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')
->where($qb->expr()->in('os.opportunity_id', $dealIds))
->orderBy('os.opportunity_id', 'ASC')
->addOrderBy('s.created_at', 'ASC')
;
return $qb->executeQuery()->fetchAllAssociative();
}
private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array
{
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$data = [
'uuid' => RequiresUUID::toNormal($row['uuid']),
'name' => $row['name'],
'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),
'account' => [
'name' => $row['acc_name'],
'url' => $crmService->generateProviderUrl(
providerId: $row['acc_provider_id'],
objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'
),
],
'owner' => null,
'rawValue' => [
'amount' => (float) $row['value'],
'currency' => $row['currency_code'],
],
'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),
'openDate' => $row['remotely_created_at'] ?? null,
'closeDate' => $row['close_date'] ?? null,
'stages' => [],
'currentPipelineId' => $row['pipeline_id'],
'currentStage' => [
'id' => $row['stage_id'],
'enteredAt' => $row['stage_updated_at'],
],
'currentStageUpdatedAt' => $row['stage_updated_at'],
'isClosed' => (bool) $row['is_closed'],
'isWon' => (bool) $row['is_won'],
];
if (isset($row['owner_uuid'])) {
$data['owner'] = [
'uuid' => RequiresUUID::toNormal($row['owner_uuid']),
'name' => $row['owner_name'],
'photoUrl' => $row['owner_photo'] === null
? null
: client_cdn($row['owner_photo'], $team),
'id' => $row['owner_id'],
'job' => $row['owner_job'],
];
}
$result[(int) $row['opp_id']] = $data;
}
return $this->hydrateStages($result);
}
private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder
{
$qb = clone $queryBuilder;
$qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');
$qb
->select(...[
'opp.id as opp_id',
'opp.uuid',
'opp.name',
'opp.value',
'opp.currency_code',
'opp.close_date',
'opp.remotely_created_at',
'opp.is_closed',
'opp.is_won',
])
->addSelect(...[
'usr.uuid as owner_uuid',
'usr.name AS owner_name',
'usr.photo_path as owner_photo',
'usr.id AS owner_id',
'jt.name as owner_job',
])
->addSelect('opp.stage_id', 'opp.stage_updated_at')
->addSelect(...[
'acc.name AS acc_name',
'acc.is_internal as acc_is_internal',
'opp.stage_updated_at',
'acc.crm_provider_id AS acc_provider_id',
'opp.crm_provider_id AS opp_provider_id',
])
->addSelect('rt.business_process_id AS pipeline_id')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'));
return $qb;
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws SocialAccountTokenInvalidException
*/
private function getCrmService(Team $team): ServiceInterface
{
$crmService = $this->providerRegistry->get($team->crm->provider);
$crmService->setConfiguration($team->crm);
if ($crmService instanceof UrlGeneratorInterface) {
$crmService->setCrmUrlGenerator($team->crm);
}
return $crmService;
}
/**
*
* @return Generator<DealData>
*/
public function getForecastData(DealsFilter $filter): Generator
{
$opportunities = DB::query()
->select([
'o.value',
'o.close_date',
'o.currency_code',
'o.is_won',
'o.is_closed',
'o.probability',
'o.forecast_category',
])
->from('opportunities', 'o')
->join('users', 'users.id', '=', 'o.user_id')
->join('groups', 'groups.id', '=', 'users.group_id')
->where('users.team_id', $filter->getTeam()->getId())
->where('o.close_date', '>=', $filter->getStartDate())
->where('o.close_date', '<=', $filter->getEndDate())
->where('o.currency_code', $filter->getCurrency())
->where('o.deleted_at', '=', null)
;
$userUuidList = $filter->getUserUuidList();
if (! empty($userUuidList)) {
$userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);
$opportunities->whereIn('users.uuid', $userUuidList);
}
$groupUuidList = $filter->getGroupUuidList();
if (! empty($groupUuidList)) {
$groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);
$opportunities->whereIn('groups.uuid', $groupUuidList);
}
foreach ($opportunities->cursor() as $row) {
yield new DealData(
(float) $row->value,
$row->close_date,
! empty($row->is_won),
! empty($row->is_closed),
$row->probability ?: 0,
$row->forecast_category ?: '',
);
}
}
public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection
{
return $user->subscriptionSets()
->where(static function (Eloquent\Builder $query): void {
$query
->whereNull('expired_at')
->orWhere('expired_at', '>=', now());
})
->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {
$join
->on('subscription_set_id', '=', 'activity_subscription_sets.id');
$join
->where('followable_type', Models\Activity\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)
->whereIn('followable_id', $opportunityIds);
})
->pluck('followable_id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20157-AJ-report-not-send-notification, menu","depth":5,"help_text":"Git Branch: JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"RequestGenerateAskJiminnyReportJobTest","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"17","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"7","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active Ask Jiminny reports whose expiry date has passed.\n *\n * @return Collection<AutomatedReport>\n */\n public function getExpiredActiveAskJiminnyReports(): Collection\n {\n return AutomatedReport::where('status', true)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereNotNull('expires_at')\n ->where('expires_at', '<', now()->toDateString())\n ->get();\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('report_id', $report->getId())\n ->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])\n ->latest()\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n \n public function countAllUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Restrict a query on the automated_reports table to reports the given user is allowed to see.\n *\n * Matches the customer-facing audience:\n * - explicit user recipients (recipients.users)\n * - members of any of the report's groups (Ask Jiminny reports)\n */\n private function applyUserAccessScope(Builder $query, User $user): void\n {\n $userId = $user->getId();\n $groupId = $user->getGroupId();\n\n $query\n ->where('automated_reports.team_id', $user->getTeamId())\n ->where(function (Builder $q) use ($userId, $groupId): void {\n $q->whereJsonContains('automated_reports.recipients->users', $userId);\n\n if ($groupId !== null) {\n $q->orWhere(function (Builder $sub) use ($groupId): void {\n $sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereJsonContains('automated_reports.groups', $groupId);\n });\n }\n });\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active Ask Jiminny reports whose expiry date has passed.\n *\n * @return Collection<AutomatedReport>\n */\n public function getExpiredActiveAskJiminnyReports(): Collection\n {\n return AutomatedReport::where('status', true)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereNotNull('expires_at')\n ->where('expires_at', '<', now()->toDateString())\n ->get();\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('report_id', $report->getId())\n ->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])\n ->latest()\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n \n public function countAllUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Restrict a query on the automated_reports table to reports the given user is allowed to see.\n *\n * Matches the customer-facing audience:\n * - explicit user recipients (recipients.users)\n * - members of any of the report's groups (Ask Jiminny reports)\n */\n private function applyUserAccessScope(Builder $query, User $user): void\n {\n $userId = $user->getId();\n $groupId = $user->getGroupId();\n\n $query\n ->where('automated_reports.team_id', $user->getTeamId())\n ->where(function (Builder $q) use ($userId, $groupId): void {\n $q->whereJsonContains('automated_reports.recipients->users', $userId);\n\n if ($groupId !== null) {\n $q->orWhere(function (Builder $sub) use ($groupId): void {\n $sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereJsonContains('automated_reports.groups', $groupId);\n });\n }\n });\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"36","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\DealInsights;\n\nuse Doctrine\\DBAL\\Connection;\nuse Generator;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealData;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealsFilter;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\QueryBuilder;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\Visitor\\QueryBuilderVisitorInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Services\\Crm\\IntegrationApp\\DTO\\Utils\\UrlGeneratorInterface;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Eloquent;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass DealsRepository implements DealsRepositoryInterface\n{\n private Connection $connection;\n\n private ProviderRegistry $providerRegistry;\n\n /**\n * @var QueryBuilderVisitorInterface[]\n */\n private array $visitors = [];\n\n /**\n * @param QueryBuilderVisitorInterface[] $visitors\n */\n public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])\n {\n $this->connection = $connection;\n $this->providerRegistry = $crmProviderRegistry;\n\n foreach ($visitors as $visitor) {\n $this->visitors[$visitor->getIdentifier()] = $visitor;\n }\n }\n\n public function getDeals(CriteriaInterface $criteria): array\n {\n $context = $criteria->getContext();\n $team = $context->getTeam();\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n\n $this->visit($qb, $criteria);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getDeal(Team $team, int $id): array\n {\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n $qb->andWhere('opp.id = :id')->setParameter('id', $id);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')\n ->from('crm_fields', 'f')\n ->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')\n ->where('f.crm_configuration_id = :crm')\n ->andWhere('f.object_type = :type')\n ->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')\n ->orderBy('fd.object_id', 'ASC')\n ->addOrderBy('fd.updated_at', 'ASC')\n\n ->setParameter('type', Field::OBJECT_OPPORTUNITY)\n ->setParameter('crm', $crmId)\n ;\n\n if (! empty($crmFields)) {\n $fields = array_map(fn ($value): string => '\"' . $value . '\"', $crmFields);\n $qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');\n }\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAssociative();\n }\n\n public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('COALESCE(opp.currency_code, \"' . $defaultCurrency . '\") AS currency')\n ->addSelect('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ->groupBy('currency')\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getDealActivities(CriteriaInterface $criteria): array\n {\n $qb = Activity::with(['participants', 'user'])\n ->where('opportunity_id', $criteria->getOpportunityId())\n ->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())\n ->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())\n ->orderBy($criteria->getSortBy(), $criteria->getSortDirection())\n ;\n\n // Should we filter activities by criteria? It's intended to filter deals.\n\n return $qb->get()->all();\n }\n\n public function getStages(CriteriaInterface $criteria): array\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('id', 'label', 'sequence')\n ->from('stages', 's')\n ->where('crm_configuration_id = :crm_configuration_id')\n ->andWhere('type = :type')\n ->orderBy('sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())\n ->setParameter('type', Stage::TYPE_OPPORTUNITY);\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $result[$row['id']] = [\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n public function getConfigurationStages(Configuration $configuration): Collection\n {\n return $configuration\n ->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get();\n }\n\n public function getPipelineData(Configuration $crm): array\n {\n $qb = new QueryBuilder($this->connection);\n $provider = $crm->provider;\n\n $qb\n ->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')\n ->from('stages', 's')\n ->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')\n ->where('s.crm_configuration_id = :crm_configuration_id')\n ->andWhere('s.type = :type')\n ->orderBy('bps.business_process_id', 'ASC')\n ->addOrderBy('s.sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $crm->id)\n ->setParameter('type', Stage::TYPE_OPPORTUNITY)\n ;\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];\n $result[$row['pipeline_id']][] = [\n 'value' => $value,\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n private function createQueryBuilder(string $realm): QueryBuilder\n {\n return (new QueryBuilder($this->connection))\n ->setRealm($realm)\n ->from('opportunities', 'opp')\n ->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')\n ->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')\n ->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')\n ;\n }\n\n /**\n * Applies all applicable visitors and returns the IDs of the executed ones\n *\n * @return string[]\n */\n private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array\n {\n $queryVisitors = [];\n\n foreach ($this->visitors as $visitor) {\n if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {\n $visitor->visit($queryBuilder, $criteria);\n\n $queryVisitors[] = $visitor->getIdentifier();\n }\n }\n\n return $queryVisitors;\n }\n\n private function hydrateStages(array $deals): array\n {\n foreach ($this->fetchStages(array_keys($deals)) as $stage) {\n $oppId = (int) $stage['opportunity_id'];\n\n if (! isset($deals[$oppId])) {\n continue; // or throw??!\n }\n\n $deals[$oppId]['stages'][] = [\n 'id' => $stage['stage_id'],\n 'name' => $stage['label'],\n 'enteredAt' => $stage['created_at'],\n ];\n }\n\n return $deals;\n }\n\n /**\n * @param int[] $dealIds\n */\n private function fetchStages(array $dealIds): array\n {\n if (empty($dealIds)) {\n return [];\n }\n\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')\n ->from('opportunity_stages', 'os')\n ->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')\n ->where($qb->expr()->in('os.opportunity_id', $dealIds))\n ->orderBy('os.opportunity_id', 'ASC')\n ->addOrderBy('s.created_at', 'ASC')\n ;\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array\n {\n $result = [];\n\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $data = [\n 'uuid' => RequiresUUID::toNormal($row['uuid']),\n 'name' => $row['name'],\n 'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),\n 'account' => [\n 'name' => $row['acc_name'],\n 'url' => $crmService->generateProviderUrl(\n providerId: $row['acc_provider_id'],\n objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'\n ),\n ],\n 'owner' => null,\n 'rawValue' => [\n 'amount' => (float) $row['value'],\n 'currency' => $row['currency_code'],\n ],\n 'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),\n 'openDate' => $row['remotely_created_at'] ?? null,\n 'closeDate' => $row['close_date'] ?? null,\n 'stages' => [],\n 'currentPipelineId' => $row['pipeline_id'],\n 'currentStage' => [\n 'id' => $row['stage_id'],\n 'enteredAt' => $row['stage_updated_at'],\n ],\n 'currentStageUpdatedAt' => $row['stage_updated_at'],\n 'isClosed' => (bool) $row['is_closed'],\n 'isWon' => (bool) $row['is_won'],\n ];\n\n if (isset($row['owner_uuid'])) {\n $data['owner'] = [\n 'uuid' => RequiresUUID::toNormal($row['owner_uuid']),\n 'name' => $row['owner_name'],\n 'photoUrl' => $row['owner_photo'] === null\n ? null\n : client_cdn($row['owner_photo'], $team),\n 'id' => $row['owner_id'],\n 'job' => $row['owner_job'],\n ];\n }\n\n $result[(int) $row['opp_id']] = $data;\n }\n\n return $this->hydrateStages($result);\n }\n\n private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder\n {\n $qb = clone $queryBuilder;\n $qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');\n\n $qb\n ->select(...[\n 'opp.id as opp_id',\n 'opp.uuid',\n 'opp.name',\n 'opp.value',\n 'opp.currency_code',\n 'opp.close_date',\n 'opp.remotely_created_at',\n 'opp.is_closed',\n 'opp.is_won',\n ])\n ->addSelect(...[\n 'usr.uuid as owner_uuid',\n 'usr.name AS owner_name',\n 'usr.photo_path as owner_photo',\n 'usr.id AS owner_id',\n 'jt.name as owner_job',\n ])\n ->addSelect('opp.stage_id', 'opp.stage_updated_at')\n ->addSelect(...[\n 'acc.name AS acc_name',\n 'acc.is_internal as acc_is_internal',\n 'opp.stage_updated_at',\n 'acc.crm_provider_id AS acc_provider_id',\n 'opp.crm_provider_id AS opp_provider_id',\n ])\n ->addSelect('rt.business_process_id AS pipeline_id')\n\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'));\n\n return $qb;\n }\n\n /**\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws SocialAccountTokenInvalidException\n */\n private function getCrmService(Team $team): ServiceInterface\n {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n $crmService->setConfiguration($team->crm);\n if ($crmService instanceof UrlGeneratorInterface) {\n $crmService->setCrmUrlGenerator($team->crm);\n }\n\n return $crmService;\n }\n\n /**\n *\n * @return Generator<DealData>\n */\n public function getForecastData(DealsFilter $filter): Generator\n {\n $opportunities = DB::query()\n ->select([\n 'o.value',\n 'o.close_date',\n 'o.currency_code',\n 'o.is_won',\n 'o.is_closed',\n 'o.probability',\n 'o.forecast_category',\n ])\n ->from('opportunities', 'o')\n ->join('users', 'users.id', '=', 'o.user_id')\n ->join('groups', 'groups.id', '=', 'users.group_id')\n ->where('users.team_id', $filter->getTeam()->getId())\n ->where('o.close_date', '>=', $filter->getStartDate())\n ->where('o.close_date', '<=', $filter->getEndDate())\n ->where('o.currency_code', $filter->getCurrency())\n ->where('o.deleted_at', '=', null)\n ;\n\n $userUuidList = $filter->getUserUuidList();\n if (! empty($userUuidList)) {\n $userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);\n\n $opportunities->whereIn('users.uuid', $userUuidList);\n }\n\n $groupUuidList = $filter->getGroupUuidList();\n if (! empty($groupUuidList)) {\n $groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);\n\n $opportunities->whereIn('groups.uuid', $groupUuidList);\n }\n\n foreach ($opportunities->cursor() as $row) {\n yield new DealData(\n (float) $row->value,\n $row->close_date,\n ! empty($row->is_won),\n ! empty($row->is_closed),\n $row->probability ?: 0,\n $row->forecast_category ?: '',\n );\n }\n }\n\n public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection\n {\n return $user->subscriptionSets()\n ->where(static function (Eloquent\\Builder $query): void {\n $query\n ->whereNull('expired_at')\n ->orWhere('expired_at', '>=', now());\n })\n ->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n $join\n ->where('followable_type', Models\\Activity\\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)\n ->whereIn('followable_id', $opportunityIds);\n })\n ->pluck('followable_id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\DealInsights;\n\nuse Doctrine\\DBAL\\Connection;\nuse Generator;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealData;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealsFilter;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\QueryBuilder;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\Visitor\\QueryBuilderVisitorInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Services\\Crm\\IntegrationApp\\DTO\\Utils\\UrlGeneratorInterface;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Eloquent;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass DealsRepository implements DealsRepositoryInterface\n{\n private Connection $connection;\n\n private ProviderRegistry $providerRegistry;\n\n /**\n * @var QueryBuilderVisitorInterface[]\n */\n private array $visitors = [];\n\n /**\n * @param QueryBuilderVisitorInterface[] $visitors\n */\n public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])\n {\n $this->connection = $connection;\n $this->providerRegistry = $crmProviderRegistry;\n\n foreach ($visitors as $visitor) {\n $this->visitors[$visitor->getIdentifier()] = $visitor;\n }\n }\n\n public function getDeals(CriteriaInterface $criteria): array\n {\n $context = $criteria->getContext();\n $team = $context->getTeam();\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n\n $this->visit($qb, $criteria);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getDeal(Team $team, int $id): array\n {\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n $qb->andWhere('opp.id = :id')->setParameter('id', $id);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')\n ->from('crm_fields', 'f')\n ->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')\n ->where('f.crm_configuration_id = :crm')\n ->andWhere('f.object_type = :type')\n ->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')\n ->orderBy('fd.object_id', 'ASC')\n ->addOrderBy('fd.updated_at', 'ASC')\n\n ->setParameter('type', Field::OBJECT_OPPORTUNITY)\n ->setParameter('crm', $crmId)\n ;\n\n if (! empty($crmFields)) {\n $fields = array_map(fn ($value): string => '\"' . $value . '\"', $crmFields);\n $qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');\n }\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAssociative();\n }\n\n public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('COALESCE(opp.currency_code, \"' . $defaultCurrency . '\") AS currency')\n ->addSelect('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ->groupBy('currency')\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getDealActivities(CriteriaInterface $criteria): array\n {\n $qb = Activity::with(['participants', 'user'])\n ->where('opportunity_id', $criteria->getOpportunityId())\n ->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())\n ->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())\n ->orderBy($criteria->getSortBy(), $criteria->getSortDirection())\n ;\n\n // Should we filter activities by criteria? It's intended to filter deals.\n\n return $qb->get()->all();\n }\n\n public function getStages(CriteriaInterface $criteria): array\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('id', 'label', 'sequence')\n ->from('stages', 's')\n ->where('crm_configuration_id = :crm_configuration_id')\n ->andWhere('type = :type')\n ->orderBy('sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())\n ->setParameter('type', Stage::TYPE_OPPORTUNITY);\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $result[$row['id']] = [\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n public function getConfigurationStages(Configuration $configuration): Collection\n {\n return $configuration\n ->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get();\n }\n\n public function getPipelineData(Configuration $crm): array\n {\n $qb = new QueryBuilder($this->connection);\n $provider = $crm->provider;\n\n $qb\n ->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')\n ->from('stages', 's')\n ->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')\n ->where('s.crm_configuration_id = :crm_configuration_id')\n ->andWhere('s.type = :type')\n ->orderBy('bps.business_process_id', 'ASC')\n ->addOrderBy('s.sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $crm->id)\n ->setParameter('type', Stage::TYPE_OPPORTUNITY)\n ;\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];\n $result[$row['pipeline_id']][] = [\n 'value' => $value,\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n private function createQueryBuilder(string $realm): QueryBuilder\n {\n return (new QueryBuilder($this->connection))\n ->setRealm($realm)\n ->from('opportunities', 'opp')\n ->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')\n ->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')\n ->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')\n ;\n }\n\n /**\n * Applies all applicable visitors and returns the IDs of the executed ones\n *\n * @return string[]\n */\n private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array\n {\n $queryVisitors = [];\n\n foreach ($this->visitors as $visitor) {\n if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {\n $visitor->visit($queryBuilder, $criteria);\n\n $queryVisitors[] = $visitor->getIdentifier();\n }\n }\n\n return $queryVisitors;\n }\n\n private function hydrateStages(array $deals): array\n {\n foreach ($this->fetchStages(array_keys($deals)) as $stage) {\n $oppId = (int) $stage['opportunity_id'];\n\n if (! isset($deals[$oppId])) {\n continue; // or throw??!\n }\n\n $deals[$oppId]['stages'][] = [\n 'id' => $stage['stage_id'],\n 'name' => $stage['label'],\n 'enteredAt' => $stage['created_at'],\n ];\n }\n\n return $deals;\n }\n\n /**\n * @param int[] $dealIds\n */\n private function fetchStages(array $dealIds): array\n {\n if (empty($dealIds)) {\n return [];\n }\n\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')\n ->from('opportunity_stages', 'os')\n ->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')\n ->where($qb->expr()->in('os.opportunity_id', $dealIds))\n ->orderBy('os.opportunity_id', 'ASC')\n ->addOrderBy('s.created_at', 'ASC')\n ;\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array\n {\n $result = [];\n\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $data = [\n 'uuid' => RequiresUUID::toNormal($row['uuid']),\n 'name' => $row['name'],\n 'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),\n 'account' => [\n 'name' => $row['acc_name'],\n 'url' => $crmService->generateProviderUrl(\n providerId: $row['acc_provider_id'],\n objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'\n ),\n ],\n 'owner' => null,\n 'rawValue' => [\n 'amount' => (float) $row['value'],\n 'currency' => $row['currency_code'],\n ],\n 'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),\n 'openDate' => $row['remotely_created_at'] ?? null,\n 'closeDate' => $row['close_date'] ?? null,\n 'stages' => [],\n 'currentPipelineId' => $row['pipeline_id'],\n 'currentStage' => [\n 'id' => $row['stage_id'],\n 'enteredAt' => $row['stage_updated_at'],\n ],\n 'currentStageUpdatedAt' => $row['stage_updated_at'],\n 'isClosed' => (bool) $row['is_closed'],\n 'isWon' => (bool) $row['is_won'],\n ];\n\n if (isset($row['owner_uuid'])) {\n $data['owner'] = [\n 'uuid' => RequiresUUID::toNormal($row['owner_uuid']),\n 'name' => $row['owner_name'],\n 'photoUrl' => $row['owner_photo'] === null\n ? null\n : client_cdn($row['owner_photo'], $team),\n 'id' => $row['owner_id'],\n 'job' => $row['owner_job'],\n ];\n }\n\n $result[(int) $row['opp_id']] = $data;\n }\n\n return $this->hydrateStages($result);\n }\n\n private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder\n {\n $qb = clone $queryBuilder;\n $qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');\n\n $qb\n ->select(...[\n 'opp.id as opp_id',\n 'opp.uuid',\n 'opp.name',\n 'opp.value',\n 'opp.currency_code',\n 'opp.close_date',\n 'opp.remotely_created_at',\n 'opp.is_closed',\n 'opp.is_won',\n ])\n ->addSelect(...[\n 'usr.uuid as owner_uuid',\n 'usr.name AS owner_name',\n 'usr.photo_path as owner_photo',\n 'usr.id AS owner_id',\n 'jt.name as owner_job',\n ])\n ->addSelect('opp.stage_id', 'opp.stage_updated_at')\n ->addSelect(...[\n 'acc.name AS acc_name',\n 'acc.is_internal as acc_is_internal',\n 'opp.stage_updated_at',\n 'acc.crm_provider_id AS acc_provider_id',\n 'opp.crm_provider_id AS opp_provider_id',\n ])\n ->addSelect('rt.business_process_id AS pipeline_id')\n\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'));\n\n return $qb;\n }\n\n /**\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws SocialAccountTokenInvalidException\n */\n private function getCrmService(Team $team): ServiceInterface\n {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n $crmService->setConfiguration($team->crm);\n if ($crmService instanceof UrlGeneratorInterface) {\n $crmService->setCrmUrlGenerator($team->crm);\n }\n\n return $crmService;\n }\n\n /**\n *\n * @return Generator<DealData>\n */\n public function getForecastData(DealsFilter $filter): Generator\n {\n $opportunities = DB::query()\n ->select([\n 'o.value',\n 'o.close_date',\n 'o.currency_code',\n 'o.is_won',\n 'o.is_closed',\n 'o.probability',\n 'o.forecast_category',\n ])\n ->from('opportunities', 'o')\n ->join('users', 'users.id', '=', 'o.user_id')\n ->join('groups', 'groups.id', '=', 'users.group_id')\n ->where('users.team_id', $filter->getTeam()->getId())\n ->where('o.close_date', '>=', $filter->getStartDate())\n ->where('o.close_date', '<=', $filter->getEndDate())\n ->where('o.currency_code', $filter->getCurrency())\n ->where('o.deleted_at', '=', null)\n ;\n\n $userUuidList = $filter->getUserUuidList();\n if (! empty($userUuidList)) {\n $userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);\n\n $opportunities->whereIn('users.uuid', $userUuidList);\n }\n\n $groupUuidList = $filter->getGroupUuidList();\n if (! empty($groupUuidList)) {\n $groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);\n\n $opportunities->whereIn('groups.uuid', $groupUuidList);\n }\n\n foreach ($opportunities->cursor() as $row) {\n yield new DealData(\n (float) $row->value,\n $row->close_date,\n ! empty($row->is_won),\n ! empty($row->is_closed),\n $row->probability ?: 0,\n $row->forecast_category ?: '',\n );\n }\n }\n\n public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection\n {\n return $user->subscriptionSets()\n ->where(static function (Eloquent\\Builder $query): void {\n $query\n ->whereNull('expired_at')\n ->orWhere('expired_at', '>=', now());\n })\n ->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n $join\n ->where('followable_type', Models\\Activity\\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)\n ->whereIn('followable_id', $opportunityIds);\n })\n ->pluck('followable_id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-2090548142966872323
|
-4167466464577769868
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
17
7
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active Ask Jiminny reports whose expiry date has passed.
*
* @return Collection<AutomatedReport>
*/
public function getExpiredActiveAskJiminnyReports(): Collection
{
return AutomatedReport::where('status', true)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereNotNull('expires_at')
->where('expires_at', '<', now()->toDateString())
->get();
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('report_id', $report->getId())
->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])
->latest()
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
public function countAllUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Restrict a query on the automated_reports table to reports the given user is allowed to see.
*
* Matches the customer-facing audience:
* - explicit user recipients (recipients.users)
* - members of any of the report's groups (Ask Jiminny reports)
*/
private function applyUserAccessScope(Builder $query, User $user): void
{
$userId = $user->getId();
$groupId = $user->getGroupId();
$query
->where('automated_reports.team_id', $user->getTeamId())
->where(function (Builder $q) use ($userId, $groupId): void {
$q->whereJsonContains('automated_reports.recipients->users', $userId);
if ($groupId !== null) {
$q->orWhere(function (Builder $sub) use ($groupId): void {
$sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereJsonContains('automated_reports.groups', $groupId);
});
}
});
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
36
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\DealInsights;
use Doctrine\DBAL\Connection;
use Generator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Jiminny\Component\DealInsights\Forecast\DealData;
use Jiminny\Component\DealInsights\Forecast\DealsFilter;
use Jiminny\Component\DealInsights\QueryBuilder\QueryBuilder;
use Jiminny\Component\DealInsights\QueryBuilder\Visitor\QueryBuilderVisitorInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Services\Crm\IntegrationApp\DTO\Utils\UrlGeneratorInterface;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class DealsRepository implements DealsRepositoryInterface
{
private Connection $connection;
private ProviderRegistry $providerRegistry;
/**
* @var QueryBuilderVisitorInterface[]
*/
private array $visitors = [];
/**
* @param QueryBuilderVisitorInterface[] $visitors
*/
public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])
{
$this->connection = $connection;
$this->providerRegistry = $crmProviderRegistry;
foreach ($visitors as $visitor) {
$this->visitors[$visitor->getIdentifier()] = $visitor;
}
}
public function getDeals(CriteriaInterface $criteria): array
{
$context = $criteria->getContext();
$team = $context->getTeam();
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$this->visit($qb, $criteria);
return $this->execute($team, $crmService, $qb);
}
public function getDeal(Team $team, int $id): array
{
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$qb->andWhere('opp.id = :id')->setParameter('id', $id);
return $this->execute($team, $crmService, $qb);
}
public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])
{
$qb = new QueryBuilder($this->connection);
$qb
->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')
->from('crm_fields', 'f')
->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')
->where('f.crm_configuration_id = :crm')
->andWhere('f.object_type = :type')
->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')
->orderBy('fd.object_id', 'ASC')
->addOrderBy('fd.updated_at', 'ASC')
->setParameter('type', Field::OBJECT_OPPORTUNITY)
->setParameter('crm', $crmId)
;
if (! empty($crmFields)) {
$fields = array_map(fn ($value): string => '"' . $value . '"', $crmFields);
$qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');
}
return $qb->executeQuery()->fetchAllAssociative();
}
public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAssociative();
}
public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('COALESCE(opp.currency_code, "' . $defaultCurrency . '") AS currency')
->addSelect('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
->groupBy('currency')
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAllAssociative();
}
public function getDealActivities(CriteriaInterface $criteria): array
{
$qb = Activity::with(['participants', 'user'])
->where('opportunity_id', $criteria->getOpportunityId())
->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())
->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())
->orderBy($criteria->getSortBy(), $criteria->getSortDirection())
;
// Should we filter activities by criteria? It's intended to filter deals.
return $qb->get()->all();
}
public function getStages(CriteriaInterface $criteria): array
{
$qb = new QueryBuilder($this->connection);
$qb
->select('id', 'label', 'sequence')
->from('stages', 's')
->where('crm_configuration_id = :crm_configuration_id')
->andWhere('type = :type')
->orderBy('sequence', 'ASC')
->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())
->setParameter('type', Stage::TYPE_OPPORTUNITY);
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$result[$row['id']] = [
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
public function getConfigurationStages(Configuration $configuration): Collection
{
return $configuration
->stages()
->where('type', Stage::TYPE_OPPORTUNITY)
->get();
}
public function getPipelineData(Configuration $crm): array
{
$qb = new QueryBuilder($this->connection);
$provider = $crm->provider;
$qb
->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')
->from('stages', 's')
->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')
->where('s.crm_configuration_id = :crm_configuration_id')
->andWhere('s.type = :type')
->orderBy('bps.business_process_id', 'ASC')
->addOrderBy('s.sequence', 'ASC')
->setParameter('crm_configuration_id', $crm->id)
->setParameter('type', Stage::TYPE_OPPORTUNITY)
;
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];
$result[$row['pipeline_id']][] = [
'value' => $value,
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
private function createQueryBuilder(string $realm): QueryBuilder
{
return (new QueryBuilder($this->connection))
->setRealm($realm)
->from('opportunities', 'opp')
->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')
->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')
->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')
;
}
/**
* Applies all applicable visitors and returns the IDs of the executed ones
*
* @return string[]
*/
private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array
{
$queryVisitors = [];
foreach ($this->visitors as $visitor) {
if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {
$visitor->visit($queryBuilder, $criteria);
$queryVisitors[] = $visitor->getIdentifier();
}
}
return $queryVisitors;
}
private function hydrateStages(array $deals): array
{
foreach ($this->fetchStages(array_keys($deals)) as $stage) {
$oppId = (int) $stage['opportunity_id'];
if (! isset($deals[$oppId])) {
continue; // or throw??!
}
$deals[$oppId]['stages'][] = [
'id' => $stage['stage_id'],
'name' => $stage['label'],
'enteredAt' => $stage['created_at'],
];
}
return $deals;
}
/**
* @param int[] $dealIds
*/
private function fetchStages(array $dealIds): array
{
if (empty($dealIds)) {
return [];
}
$qb = new QueryBuilder($this->connection);
$qb
->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')
->from('opportunity_stages', 'os')
->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')
->where($qb->expr()->in('os.opportunity_id', $dealIds))
->orderBy('os.opportunity_id', 'ASC')
->addOrderBy('s.created_at', 'ASC')
;
return $qb->executeQuery()->fetchAllAssociative();
}
private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array
{
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$data = [
'uuid' => RequiresUUID::toNormal($row['uuid']),
'name' => $row['name'],
'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),
'account' => [
'name' => $row['acc_name'],
'url' => $crmService->generateProviderUrl(
providerId: $row['acc_provider_id'],
objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'
),
],
'owner' => null,
'rawValue' => [
'amount' => (float) $row['value'],
'currency' => $row['currency_code'],
],
'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),
'openDate' => $row['remotely_created_at'] ?? null,
'closeDate' => $row['close_date'] ?? null,
'stages' => [],
'currentPipelineId' => $row['pipeline_id'],
'currentStage' => [
'id' => $row['stage_id'],
'enteredAt' => $row['stage_updated_at'],
],
'currentStageUpdatedAt' => $row['stage_updated_at'],
'isClosed' => (bool) $row['is_closed'],
'isWon' => (bool) $row['is_won'],
];
if (isset($row['owner_uuid'])) {
$data['owner'] = [
'uuid' => RequiresUUID::toNormal($row['owner_uuid']),
'name' => $row['owner_name'],
'photoUrl' => $row['owner_photo'] === null
? null
: client_cdn($row['owner_photo'], $team),
'id' => $row['owner_id'],
'job' => $row['owner_job'],
];
}
$result[(int) $row['opp_id']] = $data;
}
return $this->hydrateStages($result);
}
private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder
{
$qb = clone $queryBuilder;
$qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');
$qb
->select(...[
'opp.id as opp_id',
'opp.uuid',
'opp.name',
'opp.value',
'opp.currency_code',
'opp.close_date',
'opp.remotely_created_at',
'opp.is_closed',
'opp.is_won',
])
->addSelect(...[
'usr.uuid as owner_uuid',
'usr.name AS owner_name',
'usr.photo_path as owner_photo',
'usr.id AS owner_id',
'jt.name as owner_job',
])
->addSelect('opp.stage_id', 'opp.stage_updated_at')
->addSelect(...[
'acc.name AS acc_name',
'acc.is_internal as acc_is_internal',
'opp.stage_updated_at',
'acc.crm_provider_id AS acc_provider_id',
'opp.crm_provider_id AS opp_provider_id',
])
->addSelect('rt.business_process_id AS pipeline_id')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'));
return $qb;
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws SocialAccountTokenInvalidException
*/
private function getCrmService(Team $team): ServiceInterface
{
$crmService = $this->providerRegistry->get($team->crm->provider);
$crmService->setConfiguration($team->crm);
if ($crmService instanceof UrlGeneratorInterface) {
$crmService->setCrmUrlGenerator($team->crm);
}
return $crmService;
}
/**
*
* @return Generator<DealData>
*/
public function getForecastData(DealsFilter $filter): Generator
{
$opportunities = DB::query()
->select([
'o.value',
'o.close_date',
'o.currency_code',
'o.is_won',
'o.is_closed',
'o.probability',
'o.forecast_category',
])
->from('opportunities', 'o')
->join('users', 'users.id', '=', 'o.user_id')
->join('groups', 'groups.id', '=', 'users.group_id')
->where('users.team_id', $filter->getTeam()->getId())
->where('o.close_date', '>=', $filter->getStartDate())
->where('o.close_date', '<=', $filter->getEndDate())
->where('o.currency_code', $filter->getCurrency())
->where('o.deleted_at', '=', null)
;
$userUuidList = $filter->getUserUuidList();
if (! empty($userUuidList)) {
$userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);
$opportunities->whereIn('users.uuid', $userUuidList);
}
$groupUuidList = $filter->getGroupUuidList();
if (! empty($groupUuidList)) {
$groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);
$opportunities->whereIn('groups.uuid', $groupUuidList);
}
foreach ($opportunities->cursor() as $row) {
yield new DealData(
(float) $row->value,
$row->close_date,
! empty($row->is_won),
! empty($row->is_closed),
$row->probability ?: 0,
$row->forecast_category ?: '',
);
}
}
public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection
{
return $user->subscriptionSets()
->where(static function (Eloquent\Builder $query): void {
$query
->whereNull('expired_at')
->orWhere('expired_at', '>=', now());
})
->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {
$join
->on('subscription_set_id', '=', 'activity_subscription_sets.id');
$join
->where('followable_type', Models\Activity\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)
->whereIn('followable_id', $opportunityIds);
})
->pluck('followable_id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
|
73360
|
NULL
|
0
|
2026-04-23T07:26:10.193081+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776929170193_m2.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsRepository.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
17
7
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active Ask Jiminny reports whose expiry date has passed.
*
* @return Collection<AutomatedReport>
*/
public function getExpiredActiveAskJiminnyReports(): Collection
{
return AutomatedReport::where('status', true)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereNotNull('expires_at')
->where('expires_at', '<', now()->toDateString())
->get();
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('report_id', $report->getId())
->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])
->latest()
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
public function countAllUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Restrict a query on the automated_reports table to reports the given user is allowed to see.
*
* Matches the customer-facing audience:
* - explicit user recipients (recipients.users)
* - members of any of the report's groups (Ask Jiminny reports)
*/
private function applyUserAccessScope(Builder $query, User $user): void
{
$userId = $user->getId();
$groupId = $user->getGroupId();
$query
->where('automated_reports.team_id', $user->getTeamId())
->where(function (Builder $q) use ($userId, $groupId): void {
$q->whereJsonContains('automated_reports.recipients->users', $userId);
if ($groupId !== null) {
$q->orWhere(function (Builder $sub) use ($groupId): void {
$sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereJsonContains('automated_reports.groups', $groupId);
});
}
});
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
36
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\DealInsights;
use Doctrine\DBAL\Connection;
use Generator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Jiminny\Component\DealInsights\Forecast\DealData;
use Jiminny\Component\DealInsights\Forecast\DealsFilter;
use Jiminny\Component\DealInsights\QueryBuilder\QueryBuilder;
use Jiminny\Component\DealInsights\QueryBuilder\Visitor\QueryBuilderVisitorInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Services\Crm\IntegrationApp\DTO\Utils\UrlGeneratorInterface;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class DealsRepository implements DealsRepositoryInterface
{
private Connection $connection;
private ProviderRegistry $providerRegistry;
/**
* @var QueryBuilderVisitorInterface[]
*/
private array $visitors = [];
/**
* @param QueryBuilderVisitorInterface[] $visitors
*/
public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])
{
$this->connection = $connection;
$this->providerRegistry = $crmProviderRegistry;
foreach ($visitors as $visitor) {
$this->visitors[$visitor->getIdentifier()] = $visitor;
}
}
public function getDeals(CriteriaInterface $criteria): array
{
$context = $criteria->getContext();
$team = $context->getTeam();
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$this->visit($qb, $criteria);
return $this->execute($team, $crmService, $qb);
}
public function getDeal(Team $team, int $id): array
{
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$qb->andWhere('opp.id = :id')->setParameter('id', $id);
return $this->execute($team, $crmService, $qb);
}
public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])
{
$qb = new QueryBuilder($this->connection);
$qb
->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')
->from('crm_fields', 'f')
->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')
->where('f.crm_configuration_id = :crm')
->andWhere('f.object_type = :type')
->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')
->orderBy('fd.object_id', 'ASC')
->addOrderBy('fd.updated_at', 'ASC')
->setParameter('type', Field::OBJECT_OPPORTUNITY)
->setParameter('crm', $crmId)
;
if (! empty($crmFields)) {
$fields = array_map(fn ($value): string => '"' . $value . '"', $crmFields);
$qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');
}
return $qb->executeQuery()->fetchAllAssociative();
}
public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAssociative();
}
public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('COALESCE(opp.currency_code, "' . $defaultCurrency . '") AS currency')
->addSelect('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
->groupBy('currency')
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAllAssociative();
}
public function getDealActivities(CriteriaInterface $criteria): array
{
$qb = Activity::with(['participants', 'user'])
->where('opportunity_id', $criteria->getOpportunityId())
->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())
->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())
->orderBy($criteria->getSortBy(), $criteria->getSortDirection())
;
// Should we filter activities by criteria? It's intended to filter deals.
return $qb->get()->all();
}
public function getStages(CriteriaInterface $criteria): array
{
$qb = new QueryBuilder($this->connection);
$qb
->select('id', 'label', 'sequence')
->from('stages', 's')
->where('crm_configuration_id = :crm_configuration_id')
->andWhere('type = :type')
->orderBy('sequence', 'ASC')
->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())
->setParameter('type', Stage::TYPE_OPPORTUNITY);
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$result[$row['id']] = [
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
public function getConfigurationStages(Configuration $configuration): Collection
{
return $configuration
->stages()
->where('type', Stage::TYPE_OPPORTUNITY)
->get();
}
public function getPipelineData(Configuration $crm): array
{
$qb = new QueryBuilder($this->connection);
$provider = $crm->provider;
$qb
->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')
->from('stages', 's')
->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')
->where('s.crm_configuration_id = :crm_configuration_id')
->andWhere('s.type = :type')
->orderBy('bps.business_process_id', 'ASC')
->addOrderBy('s.sequence', 'ASC')
->setParameter('crm_configuration_id', $crm->id)
->setParameter('type', Stage::TYPE_OPPORTUNITY)
;
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];
$result[$row['pipeline_id']][] = [
'value' => $value,
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
private function createQueryBuilder(string $realm): QueryBuilder
{
return (new QueryBuilder($this->connection))
->setRealm($realm)
->from('opportunities', 'opp')
->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')
->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')
->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')
;
}
/**
* Applies all applicable visitors and returns the IDs of the executed ones
*
* @return string[]
*/
private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array
{
$queryVisitors = [];
foreach ($this->visitors as $visitor) {
if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {
$visitor->visit($queryBuilder, $criteria);
$queryVisitors[] = $visitor->getIdentifier();
}
}
return $queryVisitors;
}
private function hydrateStages(array $deals): array
{
foreach ($this->fetchStages(array_keys($deals)) as $stage) {
$oppId = (int) $stage['opportunity_id'];
if (! isset($deals[$oppId])) {
continue; // or throw??!
}
$deals[$oppId]['stages'][] = [
'id' => $stage['stage_id'],
'name' => $stage['label'],
'enteredAt' => $stage['created_at'],
];
}
return $deals;
}
/**
* @param int[] $dealIds
*/
private function fetchStages(array $dealIds): array
{
if (empty($dealIds)) {
return [];
}
$qb = new QueryBuilder($this->connection);
$qb
->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')
->from('opportunity_stages', 'os')
->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')
->where($qb->expr()->in('os.opportunity_id', $dealIds))
->orderBy('os.opportunity_id', 'ASC')
->addOrderBy('s.created_at', 'ASC')
;
return $qb->executeQuery()->fetchAllAssociative();
}
private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array
{
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$data = [
'uuid' => RequiresUUID::toNormal($row['uuid']),
'name' => $row['name'],
'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),
'account' => [
'name' => $row['acc_name'],
'url' => $crmService->generateProviderUrl(
providerId: $row['acc_provider_id'],
objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'
),
],
'owner' => null,
'rawValue' => [
'amount' => (float) $row['value'],
'currency' => $row['currency_code'],
],
'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),
'openDate' => $row['remotely_created_at'] ?? null,
'closeDate' => $row['close_date'] ?? null,
'stages' => [],
'currentPipelineId' => $row['pipeline_id'],
'currentStage' => [
'id' => $row['stage_id'],
'enteredAt' => $row['stage_updated_at'],
],
'currentStageUpdatedAt' => $row['stage_updated_at'],
'isClosed' => (bool) $row['is_closed'],
'isWon' => (bool) $row['is_won'],
];
if (isset($row['owner_uuid'])) {
$data['owner'] = [
'uuid' => RequiresUUID::toNormal($row['owner_uuid']),
'name' => $row['owner_name'],
'photoUrl' => $row['owner_photo'] === null
? null
: client_cdn($row['owner_photo'], $team),
'id' => $row['owner_id'],
'job' => $row['owner_job'],
];
}
$result[(int) $row['opp_id']] = $data;
}
return $this->hydrateStages($result);
}
private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder
{
$qb = clone $queryBuilder;
$qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');
$qb
->select(...[
'opp.id as opp_id',
'opp.uuid',
'opp.name',
'opp.value',
'opp.currency_code',
'opp.close_date',
'opp.remotely_created_at',
'opp.is_closed',
'opp.is_won',
])
->addSelect(...[
'usr.uuid as owner_uuid',
'usr.name AS owner_name',
'usr.photo_path as owner_photo',
'usr.id AS owner_id',
'jt.name as owner_job',
])
->addSelect('opp.stage_id', 'opp.stage_updated_at')
->addSelect(...[
'acc.name AS acc_name',
'acc.is_internal as acc_is_internal',
'opp.stage_updated_at',
'acc.crm_provider_id AS acc_provider_id',
'opp.crm_provider_id AS opp_provider_id',
])
->addSelect('rt.business_process_id AS pipeline_id')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'));
return $qb;
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws SocialAccountTokenInvalidException
*/
private function getCrmService(Team $team): ServiceInterface
{
$crmService = $this->providerRegistry->get($team->crm->provider);
$crmService->setConfiguration($team->crm);
if ($crmService instanceof UrlGeneratorInterface) {
$crmService->setCrmUrlGenerator($team->crm);
}
return $crmService;
}
/**
*
* @return Generator<DealData>
*/
public function getForecastData(DealsFilter $filter): Generator
{
$opportunities = DB::query()
->select([
'o.value',
'o.close_date',
'o.currency_code',
'o.is_won',
'o.is_closed',
'o.probability',
'o.forecast_category',
])
->from('opportunities', 'o')
->join('users', 'users.id', '=', 'o.user_id')
->join('groups', 'groups.id', '=', 'users.group_id')
->where('users.team_id', $filter->getTeam()->getId())
->where('o.close_date', '>=', $filter->getStartDate())
->where('o.close_date', '<=', $filter->getEndDate())
->where('o.currency_code', $filter->getCurrency())
->where('o.deleted_at', '=', null)
;
$userUuidList = $filter->getUserUuidList();
if (! empty($userUuidList)) {
$userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);
$opportunities->whereIn('users.uuid', $userUuidList);
}
$groupUuidList = $filter->getGroupUuidList();
if (! empty($groupUuidList)) {
$groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);
$opportunities->whereIn('groups.uuid', $groupUuidList);
}
foreach ($opportunities->cursor() as $row) {
yield new DealData(
(float) $row->value,
$row->close_date,
! empty($row->is_won),
! empty($row->is_closed),
$row->probability ?: 0,
$row->forecast_category ?: '',
);
}
}
public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection
{
return $user->subscriptionSets()
->where(static function (Eloquent\Builder $query): void {
$query
->whereNull('expired_at')
->orWhere('expired_at', '>=', now());
})
->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {
$join
->on('subscription_set_id', '=', 'activity_subscription_sets.id');
$join
->where('followable_type', Models\Activity\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)
->whereIn('followable_id', $opportunityIds);
})
->pluck('followable_id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.25731382,"top":0.019952115,"width":0.03856383,"height":0.025538707},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20157-AJ-report-not-send-notification, menu","depth":5,"bounds":{"left":0.29587767,"top":0.019952115,"width":0.10139628,"height":0.025538707},"help_text":"Git Branch: JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.7972075,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"RequestGenerateAskJiminnyReportJobTest","depth":6,"bounds":{"left":0.8125,"top":0.019952115,"width":0.10305851,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"17","depth":4,"bounds":{"left":0.60339093,"top":0.17478053,"width":0.00930851,"height":0.015163607},"role_description":"text"},{"role":"AXStaticText","text":"7","depth":4,"bounds":{"left":0.6146942,"top":0.17478053,"width":0.0076462766,"height":0.015163607},"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.6243351,"top":0.17478053,"width":0.00731383,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.63331115,"top":0.17318435,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.640625,"top":0.17318435,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active Ask Jiminny reports whose expiry date has passed.\n *\n * @return Collection<AutomatedReport>\n */\n public function getExpiredActiveAskJiminnyReports(): Collection\n {\n return AutomatedReport::where('status', true)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereNotNull('expires_at')\n ->where('expires_at', '<', now()->toDateString())\n ->get();\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('report_id', $report->getId())\n ->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])\n ->latest()\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n \n public function countAllUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Restrict a query on the automated_reports table to reports the given user is allowed to see.\n *\n * Matches the customer-facing audience:\n * - explicit user recipients (recipients.users)\n * - members of any of the report's groups (Ask Jiminny reports)\n */\n private function applyUserAccessScope(Builder $query, User $user): void\n {\n $userId = $user->getId();\n $groupId = $user->getGroupId();\n\n $query\n ->where('automated_reports.team_id', $user->getTeamId())\n ->where(function (Builder $q) use ($userId, $groupId): void {\n $q->whereJsonContains('automated_reports.recipients->users', $userId);\n\n if ($groupId !== null) {\n $q->orWhere(function (Builder $sub) use ($groupId): void {\n $sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereJsonContains('automated_reports.groups', $groupId);\n });\n }\n });\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active Ask Jiminny reports whose expiry date has passed.\n *\n * @return Collection<AutomatedReport>\n */\n public function getExpiredActiveAskJiminnyReports(): Collection\n {\n return AutomatedReport::where('status', true)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereNotNull('expires_at')\n ->where('expires_at', '<', now()->toDateString())\n ->get();\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('report_id', $report->getId())\n ->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])\n ->latest()\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n \n public function countAllUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Restrict a query on the automated_reports table to reports the given user is allowed to see.\n *\n * Matches the customer-facing audience:\n * - explicit user recipients (recipients.users)\n * - members of any of the report's groups (Ask Jiminny reports)\n */\n private function applyUserAccessScope(Builder $query, User $user): void\n {\n $userId = $user->getId();\n $groupId = $user->getGroupId();\n\n $query\n ->where('automated_reports.team_id', $user->getTeamId())\n ->where(function (Builder $q) use ($userId, $groupId): void {\n $q->whereJsonContains('automated_reports.recipients->users', $userId);\n\n if ($groupId !== null) {\n $q->orWhere(function (Builder $sub) use ($groupId): void {\n $sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereJsonContains('automated_reports.groups', $groupId);\n });\n }\n });\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"36","depth":4,"bounds":{"left":0.69980055,"top":0.2490024,"width":0.010305851,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7117686,"top":0.24740623,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7190825,"top":0.24740623,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\DealInsights;\n\nuse Doctrine\\DBAL\\Connection;\nuse Generator;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealData;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealsFilter;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\QueryBuilder;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\Visitor\\QueryBuilderVisitorInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Services\\Crm\\IntegrationApp\\DTO\\Utils\\UrlGeneratorInterface;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Eloquent;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass DealsRepository implements DealsRepositoryInterface\n{\n private Connection $connection;\n\n private ProviderRegistry $providerRegistry;\n\n /**\n * @var QueryBuilderVisitorInterface[]\n */\n private array $visitors = [];\n\n /**\n * @param QueryBuilderVisitorInterface[] $visitors\n */\n public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])\n {\n $this->connection = $connection;\n $this->providerRegistry = $crmProviderRegistry;\n\n foreach ($visitors as $visitor) {\n $this->visitors[$visitor->getIdentifier()] = $visitor;\n }\n }\n\n public function getDeals(CriteriaInterface $criteria): array\n {\n $context = $criteria->getContext();\n $team = $context->getTeam();\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n\n $this->visit($qb, $criteria);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getDeal(Team $team, int $id): array\n {\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n $qb->andWhere('opp.id = :id')->setParameter('id', $id);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')\n ->from('crm_fields', 'f')\n ->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')\n ->where('f.crm_configuration_id = :crm')\n ->andWhere('f.object_type = :type')\n ->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')\n ->orderBy('fd.object_id', 'ASC')\n ->addOrderBy('fd.updated_at', 'ASC')\n\n ->setParameter('type', Field::OBJECT_OPPORTUNITY)\n ->setParameter('crm', $crmId)\n ;\n\n if (! empty($crmFields)) {\n $fields = array_map(fn ($value): string => '\"' . $value . '\"', $crmFields);\n $qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');\n }\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAssociative();\n }\n\n public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('COALESCE(opp.currency_code, \"' . $defaultCurrency . '\") AS currency')\n ->addSelect('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ->groupBy('currency')\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getDealActivities(CriteriaInterface $criteria): array\n {\n $qb = Activity::with(['participants', 'user'])\n ->where('opportunity_id', $criteria->getOpportunityId())\n ->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())\n ->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())\n ->orderBy($criteria->getSortBy(), $criteria->getSortDirection())\n ;\n\n // Should we filter activities by criteria? It's intended to filter deals.\n\n return $qb->get()->all();\n }\n\n public function getStages(CriteriaInterface $criteria): array\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('id', 'label', 'sequence')\n ->from('stages', 's')\n ->where('crm_configuration_id = :crm_configuration_id')\n ->andWhere('type = :type')\n ->orderBy('sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())\n ->setParameter('type', Stage::TYPE_OPPORTUNITY);\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $result[$row['id']] = [\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n public function getConfigurationStages(Configuration $configuration): Collection\n {\n return $configuration\n ->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get();\n }\n\n public function getPipelineData(Configuration $crm): array\n {\n $qb = new QueryBuilder($this->connection);\n $provider = $crm->provider;\n\n $qb\n ->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')\n ->from('stages', 's')\n ->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')\n ->where('s.crm_configuration_id = :crm_configuration_id')\n ->andWhere('s.type = :type')\n ->orderBy('bps.business_process_id', 'ASC')\n ->addOrderBy('s.sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $crm->id)\n ->setParameter('type', Stage::TYPE_OPPORTUNITY)\n ;\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];\n $result[$row['pipeline_id']][] = [\n 'value' => $value,\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n private function createQueryBuilder(string $realm): QueryBuilder\n {\n return (new QueryBuilder($this->connection))\n ->setRealm($realm)\n ->from('opportunities', 'opp')\n ->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')\n ->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')\n ->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')\n ;\n }\n\n /**\n * Applies all applicable visitors and returns the IDs of the executed ones\n *\n * @return string[]\n */\n private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array\n {\n $queryVisitors = [];\n\n foreach ($this->visitors as $visitor) {\n if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {\n $visitor->visit($queryBuilder, $criteria);\n\n $queryVisitors[] = $visitor->getIdentifier();\n }\n }\n\n return $queryVisitors;\n }\n\n private function hydrateStages(array $deals): array\n {\n foreach ($this->fetchStages(array_keys($deals)) as $stage) {\n $oppId = (int) $stage['opportunity_id'];\n\n if (! isset($deals[$oppId])) {\n continue; // or throw??!\n }\n\n $deals[$oppId]['stages'][] = [\n 'id' => $stage['stage_id'],\n 'name' => $stage['label'],\n 'enteredAt' => $stage['created_at'],\n ];\n }\n\n return $deals;\n }\n\n /**\n * @param int[] $dealIds\n */\n private function fetchStages(array $dealIds): array\n {\n if (empty($dealIds)) {\n return [];\n }\n\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')\n ->from('opportunity_stages', 'os')\n ->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')\n ->where($qb->expr()->in('os.opportunity_id', $dealIds))\n ->orderBy('os.opportunity_id', 'ASC')\n ->addOrderBy('s.created_at', 'ASC')\n ;\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array\n {\n $result = [];\n\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $data = [\n 'uuid' => RequiresUUID::toNormal($row['uuid']),\n 'name' => $row['name'],\n 'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),\n 'account' => [\n 'name' => $row['acc_name'],\n 'url' => $crmService->generateProviderUrl(\n providerId: $row['acc_provider_id'],\n objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'\n ),\n ],\n 'owner' => null,\n 'rawValue' => [\n 'amount' => (float) $row['value'],\n 'currency' => $row['currency_code'],\n ],\n 'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),\n 'openDate' => $row['remotely_created_at'] ?? null,\n 'closeDate' => $row['close_date'] ?? null,\n 'stages' => [],\n 'currentPipelineId' => $row['pipeline_id'],\n 'currentStage' => [\n 'id' => $row['stage_id'],\n 'enteredAt' => $row['stage_updated_at'],\n ],\n 'currentStageUpdatedAt' => $row['stage_updated_at'],\n 'isClosed' => (bool) $row['is_closed'],\n 'isWon' => (bool) $row['is_won'],\n ];\n\n if (isset($row['owner_uuid'])) {\n $data['owner'] = [\n 'uuid' => RequiresUUID::toNormal($row['owner_uuid']),\n 'name' => $row['owner_name'],\n 'photoUrl' => $row['owner_photo'] === null\n ? null\n : client_cdn($row['owner_photo'], $team),\n 'id' => $row['owner_id'],\n 'job' => $row['owner_job'],\n ];\n }\n\n $result[(int) $row['opp_id']] = $data;\n }\n\n return $this->hydrateStages($result);\n }\n\n private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder\n {\n $qb = clone $queryBuilder;\n $qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');\n\n $qb\n ->select(...[\n 'opp.id as opp_id',\n 'opp.uuid',\n 'opp.name',\n 'opp.value',\n 'opp.currency_code',\n 'opp.close_date',\n 'opp.remotely_created_at',\n 'opp.is_closed',\n 'opp.is_won',\n ])\n ->addSelect(...[\n 'usr.uuid as owner_uuid',\n 'usr.name AS owner_name',\n 'usr.photo_path as owner_photo',\n 'usr.id AS owner_id',\n 'jt.name as owner_job',\n ])\n ->addSelect('opp.stage_id', 'opp.stage_updated_at')\n ->addSelect(...[\n 'acc.name AS acc_name',\n 'acc.is_internal as acc_is_internal',\n 'opp.stage_updated_at',\n 'acc.crm_provider_id AS acc_provider_id',\n 'opp.crm_provider_id AS opp_provider_id',\n ])\n ->addSelect('rt.business_process_id AS pipeline_id')\n\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'));\n\n return $qb;\n }\n\n /**\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws SocialAccountTokenInvalidException\n */\n private function getCrmService(Team $team): ServiceInterface\n {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n $crmService->setConfiguration($team->crm);\n if ($crmService instanceof UrlGeneratorInterface) {\n $crmService->setCrmUrlGenerator($team->crm);\n }\n\n return $crmService;\n }\n\n /**\n *\n * @return Generator<DealData>\n */\n public function getForecastData(DealsFilter $filter): Generator\n {\n $opportunities = DB::query()\n ->select([\n 'o.value',\n 'o.close_date',\n 'o.currency_code',\n 'o.is_won',\n 'o.is_closed',\n 'o.probability',\n 'o.forecast_category',\n ])\n ->from('opportunities', 'o')\n ->join('users', 'users.id', '=', 'o.user_id')\n ->join('groups', 'groups.id', '=', 'users.group_id')\n ->where('users.team_id', $filter->getTeam()->getId())\n ->where('o.close_date', '>=', $filter->getStartDate())\n ->where('o.close_date', '<=', $filter->getEndDate())\n ->where('o.currency_code', $filter->getCurrency())\n ->where('o.deleted_at', '=', null)\n ;\n\n $userUuidList = $filter->getUserUuidList();\n if (! empty($userUuidList)) {\n $userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);\n\n $opportunities->whereIn('users.uuid', $userUuidList);\n }\n\n $groupUuidList = $filter->getGroupUuidList();\n if (! empty($groupUuidList)) {\n $groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);\n\n $opportunities->whereIn('groups.uuid', $groupUuidList);\n }\n\n foreach ($opportunities->cursor() as $row) {\n yield new DealData(\n (float) $row->value,\n $row->close_date,\n ! empty($row->is_won),\n ! empty($row->is_closed),\n $row->probability ?: 0,\n $row->forecast_category ?: '',\n );\n }\n }\n\n public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection\n {\n return $user->subscriptionSets()\n ->where(static function (Eloquent\\Builder $query): void {\n $query\n ->whereNull('expired_at')\n ->orWhere('expired_at', '>=', now());\n })\n ->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n $join\n ->where('followable_type', Models\\Activity\\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)\n ->whereIn('followable_id', $opportunityIds);\n })\n ->pluck('followable_id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\DealInsights;\n\nuse Doctrine\\DBAL\\Connection;\nuse Generator;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealData;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealsFilter;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\QueryBuilder;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\Visitor\\QueryBuilderVisitorInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Services\\Crm\\IntegrationApp\\DTO\\Utils\\UrlGeneratorInterface;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Eloquent;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass DealsRepository implements DealsRepositoryInterface\n{\n private Connection $connection;\n\n private ProviderRegistry $providerRegistry;\n\n /**\n * @var QueryBuilderVisitorInterface[]\n */\n private array $visitors = [];\n\n /**\n * @param QueryBuilderVisitorInterface[] $visitors\n */\n public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])\n {\n $this->connection = $connection;\n $this->providerRegistry = $crmProviderRegistry;\n\n foreach ($visitors as $visitor) {\n $this->visitors[$visitor->getIdentifier()] = $visitor;\n }\n }\n\n public function getDeals(CriteriaInterface $criteria): array\n {\n $context = $criteria->getContext();\n $team = $context->getTeam();\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n\n $this->visit($qb, $criteria);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getDeal(Team $team, int $id): array\n {\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n $qb->andWhere('opp.id = :id')->setParameter('id', $id);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')\n ->from('crm_fields', 'f')\n ->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')\n ->where('f.crm_configuration_id = :crm')\n ->andWhere('f.object_type = :type')\n ->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')\n ->orderBy('fd.object_id', 'ASC')\n ->addOrderBy('fd.updated_at', 'ASC')\n\n ->setParameter('type', Field::OBJECT_OPPORTUNITY)\n ->setParameter('crm', $crmId)\n ;\n\n if (! empty($crmFields)) {\n $fields = array_map(fn ($value): string => '\"' . $value . '\"', $crmFields);\n $qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');\n }\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAssociative();\n }\n\n public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('COALESCE(opp.currency_code, \"' . $defaultCurrency . '\") AS currency')\n ->addSelect('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ->groupBy('currency')\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getDealActivities(CriteriaInterface $criteria): array\n {\n $qb = Activity::with(['participants', 'user'])\n ->where('opportunity_id', $criteria->getOpportunityId())\n ->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())\n ->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())\n ->orderBy($criteria->getSortBy(), $criteria->getSortDirection())\n ;\n\n // Should we filter activities by criteria? It's intended to filter deals.\n\n return $qb->get()->all();\n }\n\n public function getStages(CriteriaInterface $criteria): array\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('id', 'label', 'sequence')\n ->from('stages', 's')\n ->where('crm_configuration_id = :crm_configuration_id')\n ->andWhere('type = :type')\n ->orderBy('sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())\n ->setParameter('type', Stage::TYPE_OPPORTUNITY);\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $result[$row['id']] = [\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n public function getConfigurationStages(Configuration $configuration): Collection\n {\n return $configuration\n ->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get();\n }\n\n public function getPipelineData(Configuration $crm): array\n {\n $qb = new QueryBuilder($this->connection);\n $provider = $crm->provider;\n\n $qb\n ->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')\n ->from('stages', 's')\n ->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')\n ->where('s.crm_configuration_id = :crm_configuration_id')\n ->andWhere('s.type = :type')\n ->orderBy('bps.business_process_id', 'ASC')\n ->addOrderBy('s.sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $crm->id)\n ->setParameter('type', Stage::TYPE_OPPORTUNITY)\n ;\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];\n $result[$row['pipeline_id']][] = [\n 'value' => $value,\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n private function createQueryBuilder(string $realm): QueryBuilder\n {\n return (new QueryBuilder($this->connection))\n ->setRealm($realm)\n ->from('opportunities', 'opp')\n ->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')\n ->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')\n ->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')\n ;\n }\n\n /**\n * Applies all applicable visitors and returns the IDs of the executed ones\n *\n * @return string[]\n */\n private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array\n {\n $queryVisitors = [];\n\n foreach ($this->visitors as $visitor) {\n if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {\n $visitor->visit($queryBuilder, $criteria);\n\n $queryVisitors[] = $visitor->getIdentifier();\n }\n }\n\n return $queryVisitors;\n }\n\n private function hydrateStages(array $deals): array\n {\n foreach ($this->fetchStages(array_keys($deals)) as $stage) {\n $oppId = (int) $stage['opportunity_id'];\n\n if (! isset($deals[$oppId])) {\n continue; // or throw??!\n }\n\n $deals[$oppId]['stages'][] = [\n 'id' => $stage['stage_id'],\n 'name' => $stage['label'],\n 'enteredAt' => $stage['created_at'],\n ];\n }\n\n return $deals;\n }\n\n /**\n * @param int[] $dealIds\n */\n private function fetchStages(array $dealIds): array\n {\n if (empty($dealIds)) {\n return [];\n }\n\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')\n ->from('opportunity_stages', 'os')\n ->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')\n ->where($qb->expr()->in('os.opportunity_id', $dealIds))\n ->orderBy('os.opportunity_id', 'ASC')\n ->addOrderBy('s.created_at', 'ASC')\n ;\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array\n {\n $result = [];\n\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $data = [\n 'uuid' => RequiresUUID::toNormal($row['uuid']),\n 'name' => $row['name'],\n 'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),\n 'account' => [\n 'name' => $row['acc_name'],\n 'url' => $crmService->generateProviderUrl(\n providerId: $row['acc_provider_id'],\n objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'\n ),\n ],\n 'owner' => null,\n 'rawValue' => [\n 'amount' => (float) $row['value'],\n 'currency' => $row['currency_code'],\n ],\n 'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),\n 'openDate' => $row['remotely_created_at'] ?? null,\n 'closeDate' => $row['close_date'] ?? null,\n 'stages' => [],\n 'currentPipelineId' => $row['pipeline_id'],\n 'currentStage' => [\n 'id' => $row['stage_id'],\n 'enteredAt' => $row['stage_updated_at'],\n ],\n 'currentStageUpdatedAt' => $row['stage_updated_at'],\n 'isClosed' => (bool) $row['is_closed'],\n 'isWon' => (bool) $row['is_won'],\n ];\n\n if (isset($row['owner_uuid'])) {\n $data['owner'] = [\n 'uuid' => RequiresUUID::toNormal($row['owner_uuid']),\n 'name' => $row['owner_name'],\n 'photoUrl' => $row['owner_photo'] === null\n ? null\n : client_cdn($row['owner_photo'], $team),\n 'id' => $row['owner_id'],\n 'job' => $row['owner_job'],\n ];\n }\n\n $result[(int) $row['opp_id']] = $data;\n }\n\n return $this->hydrateStages($result);\n }\n\n private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder\n {\n $qb = clone $queryBuilder;\n $qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');\n\n $qb\n ->select(...[\n 'opp.id as opp_id',\n 'opp.uuid',\n 'opp.name',\n 'opp.value',\n 'opp.currency_code',\n 'opp.close_date',\n 'opp.remotely_created_at',\n 'opp.is_closed',\n 'opp.is_won',\n ])\n ->addSelect(...[\n 'usr.uuid as owner_uuid',\n 'usr.name AS owner_name',\n 'usr.photo_path as owner_photo',\n 'usr.id AS owner_id',\n 'jt.name as owner_job',\n ])\n ->addSelect('opp.stage_id', 'opp.stage_updated_at')\n ->addSelect(...[\n 'acc.name AS acc_name',\n 'acc.is_internal as acc_is_internal',\n 'opp.stage_updated_at',\n 'acc.crm_provider_id AS acc_provider_id',\n 'opp.crm_provider_id AS opp_provider_id',\n ])\n ->addSelect('rt.business_process_id AS pipeline_id')\n\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'));\n\n return $qb;\n }\n\n /**\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws SocialAccountTokenInvalidException\n */\n private function getCrmService(Team $team): ServiceInterface\n {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n $crmService->setConfiguration($team->crm);\n if ($crmService instanceof UrlGeneratorInterface) {\n $crmService->setCrmUrlGenerator($team->crm);\n }\n\n return $crmService;\n }\n\n /**\n *\n * @return Generator<DealData>\n */\n public function getForecastData(DealsFilter $filter): Generator\n {\n $opportunities = DB::query()\n ->select([\n 'o.value',\n 'o.close_date',\n 'o.currency_code',\n 'o.is_won',\n 'o.is_closed',\n 'o.probability',\n 'o.forecast_category',\n ])\n ->from('opportunities', 'o')\n ->join('users', 'users.id', '=', 'o.user_id')\n ->join('groups', 'groups.id', '=', 'users.group_id')\n ->where('users.team_id', $filter->getTeam()->getId())\n ->where('o.close_date', '>=', $filter->getStartDate())\n ->where('o.close_date', '<=', $filter->getEndDate())\n ->where('o.currency_code', $filter->getCurrency())\n ->where('o.deleted_at', '=', null)\n ;\n\n $userUuidList = $filter->getUserUuidList();\n if (! empty($userUuidList)) {\n $userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);\n\n $opportunities->whereIn('users.uuid', $userUuidList);\n }\n\n $groupUuidList = $filter->getGroupUuidList();\n if (! empty($groupUuidList)) {\n $groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);\n\n $opportunities->whereIn('groups.uuid', $groupUuidList);\n }\n\n foreach ($opportunities->cursor() as $row) {\n yield new DealData(\n (float) $row->value,\n $row->close_date,\n ! empty($row->is_won),\n ! empty($row->is_closed),\n $row->probability ?: 0,\n $row->forecast_category ?: '',\n );\n }\n }\n\n public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection\n {\n return $user->subscriptionSets()\n ->where(static function (Eloquent\\Builder $query): void {\n $query\n ->whereNull('expired_at')\n ->orWhere('expired_at', '>=', now());\n })\n ->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n $join\n ->where('followable_type', Models\\Activity\\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)\n ->whereIn('followable_id', $opportunityIds);\n })\n ->pluck('followable_id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.24335106,"top":0.047885075,"width":0.024268618,"height":0.024740623},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-2090548142966872323
|
-4167466464577769868
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
17
7
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active Ask Jiminny reports whose expiry date has passed.
*
* @return Collection<AutomatedReport>
*/
public function getExpiredActiveAskJiminnyReports(): Collection
{
return AutomatedReport::where('status', true)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereNotNull('expires_at')
->where('expires_at', '<', now()->toDateString())
->get();
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('report_id', $report->getId())
->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])
->latest()
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
public function countAllUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Restrict a query on the automated_reports table to reports the given user is allowed to see.
*
* Matches the customer-facing audience:
* - explicit user recipients (recipients.users)
* - members of any of the report's groups (Ask Jiminny reports)
*/
private function applyUserAccessScope(Builder $query, User $user): void
{
$userId = $user->getId();
$groupId = $user->getGroupId();
$query
->where('automated_reports.team_id', $user->getTeamId())
->where(function (Builder $q) use ($userId, $groupId): void {
$q->whereJsonContains('automated_reports.recipients->users', $userId);
if ($groupId !== null) {
$q->orWhere(function (Builder $sub) use ($groupId): void {
$sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereJsonContains('automated_reports.groups', $groupId);
});
}
});
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
36
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\DealInsights;
use Doctrine\DBAL\Connection;
use Generator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Jiminny\Component\DealInsights\Forecast\DealData;
use Jiminny\Component\DealInsights\Forecast\DealsFilter;
use Jiminny\Component\DealInsights\QueryBuilder\QueryBuilder;
use Jiminny\Component\DealInsights\QueryBuilder\Visitor\QueryBuilderVisitorInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Services\Crm\IntegrationApp\DTO\Utils\UrlGeneratorInterface;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class DealsRepository implements DealsRepositoryInterface
{
private Connection $connection;
private ProviderRegistry $providerRegistry;
/**
* @var QueryBuilderVisitorInterface[]
*/
private array $visitors = [];
/**
* @param QueryBuilderVisitorInterface[] $visitors
*/
public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])
{
$this->connection = $connection;
$this->providerRegistry = $crmProviderRegistry;
foreach ($visitors as $visitor) {
$this->visitors[$visitor->getIdentifier()] = $visitor;
}
}
public function getDeals(CriteriaInterface $criteria): array
{
$context = $criteria->getContext();
$team = $context->getTeam();
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$this->visit($qb, $criteria);
return $this->execute($team, $crmService, $qb);
}
public function getDeal(Team $team, int $id): array
{
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$qb->andWhere('opp.id = :id')->setParameter('id', $id);
return $this->execute($team, $crmService, $qb);
}
public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])
{
$qb = new QueryBuilder($this->connection);
$qb
->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')
->from('crm_fields', 'f')
->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')
->where('f.crm_configuration_id = :crm')
->andWhere('f.object_type = :type')
->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')
->orderBy('fd.object_id', 'ASC')
->addOrderBy('fd.updated_at', 'ASC')
->setParameter('type', Field::OBJECT_OPPORTUNITY)
->setParameter('crm', $crmId)
;
if (! empty($crmFields)) {
$fields = array_map(fn ($value): string => '"' . $value . '"', $crmFields);
$qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');
}
return $qb->executeQuery()->fetchAllAssociative();
}
public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAssociative();
}
public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('COALESCE(opp.currency_code, "' . $defaultCurrency . '") AS currency')
->addSelect('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
->groupBy('currency')
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAllAssociative();
}
public function getDealActivities(CriteriaInterface $criteria): array
{
$qb = Activity::with(['participants', 'user'])
->where('opportunity_id', $criteria->getOpportunityId())
->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())
->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())
->orderBy($criteria->getSortBy(), $criteria->getSortDirection())
;
// Should we filter activities by criteria? It's intended to filter deals.
return $qb->get()->all();
}
public function getStages(CriteriaInterface $criteria): array
{
$qb = new QueryBuilder($this->connection);
$qb
->select('id', 'label', 'sequence')
->from('stages', 's')
->where('crm_configuration_id = :crm_configuration_id')
->andWhere('type = :type')
->orderBy('sequence', 'ASC')
->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())
->setParameter('type', Stage::TYPE_OPPORTUNITY);
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$result[$row['id']] = [
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
public function getConfigurationStages(Configuration $configuration): Collection
{
return $configuration
->stages()
->where('type', Stage::TYPE_OPPORTUNITY)
->get();
}
public function getPipelineData(Configuration $crm): array
{
$qb = new QueryBuilder($this->connection);
$provider = $crm->provider;
$qb
->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')
->from('stages', 's')
->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')
->where('s.crm_configuration_id = :crm_configuration_id')
->andWhere('s.type = :type')
->orderBy('bps.business_process_id', 'ASC')
->addOrderBy('s.sequence', 'ASC')
->setParameter('crm_configuration_id', $crm->id)
->setParameter('type', Stage::TYPE_OPPORTUNITY)
;
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];
$result[$row['pipeline_id']][] = [
'value' => $value,
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
private function createQueryBuilder(string $realm): QueryBuilder
{
return (new QueryBuilder($this->connection))
->setRealm($realm)
->from('opportunities', 'opp')
->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')
->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')
->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')
;
}
/**
* Applies all applicable visitors and returns the IDs of the executed ones
*
* @return string[]
*/
private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array
{
$queryVisitors = [];
foreach ($this->visitors as $visitor) {
if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {
$visitor->visit($queryBuilder, $criteria);
$queryVisitors[] = $visitor->getIdentifier();
}
}
return $queryVisitors;
}
private function hydrateStages(array $deals): array
{
foreach ($this->fetchStages(array_keys($deals)) as $stage) {
$oppId = (int) $stage['opportunity_id'];
if (! isset($deals[$oppId])) {
continue; // or throw??!
}
$deals[$oppId]['stages'][] = [
'id' => $stage['stage_id'],
'name' => $stage['label'],
'enteredAt' => $stage['created_at'],
];
}
return $deals;
}
/**
* @param int[] $dealIds
*/
private function fetchStages(array $dealIds): array
{
if (empty($dealIds)) {
return [];
}
$qb = new QueryBuilder($this->connection);
$qb
->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')
->from('opportunity_stages', 'os')
->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')
->where($qb->expr()->in('os.opportunity_id', $dealIds))
->orderBy('os.opportunity_id', 'ASC')
->addOrderBy('s.created_at', 'ASC')
;
return $qb->executeQuery()->fetchAllAssociative();
}
private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array
{
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$data = [
'uuid' => RequiresUUID::toNormal($row['uuid']),
'name' => $row['name'],
'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),
'account' => [
'name' => $row['acc_name'],
'url' => $crmService->generateProviderUrl(
providerId: $row['acc_provider_id'],
objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'
),
],
'owner' => null,
'rawValue' => [
'amount' => (float) $row['value'],
'currency' => $row['currency_code'],
],
'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),
'openDate' => $row['remotely_created_at'] ?? null,
'closeDate' => $row['close_date'] ?? null,
'stages' => [],
'currentPipelineId' => $row['pipeline_id'],
'currentStage' => [
'id' => $row['stage_id'],
'enteredAt' => $row['stage_updated_at'],
],
'currentStageUpdatedAt' => $row['stage_updated_at'],
'isClosed' => (bool) $row['is_closed'],
'isWon' => (bool) $row['is_won'],
];
if (isset($row['owner_uuid'])) {
$data['owner'] = [
'uuid' => RequiresUUID::toNormal($row['owner_uuid']),
'name' => $row['owner_name'],
'photoUrl' => $row['owner_photo'] === null
? null
: client_cdn($row['owner_photo'], $team),
'id' => $row['owner_id'],
'job' => $row['owner_job'],
];
}
$result[(int) $row['opp_id']] = $data;
}
return $this->hydrateStages($result);
}
private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder
{
$qb = clone $queryBuilder;
$qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');
$qb
->select(...[
'opp.id as opp_id',
'opp.uuid',
'opp.name',
'opp.value',
'opp.currency_code',
'opp.close_date',
'opp.remotely_created_at',
'opp.is_closed',
'opp.is_won',
])
->addSelect(...[
'usr.uuid as owner_uuid',
'usr.name AS owner_name',
'usr.photo_path as owner_photo',
'usr.id AS owner_id',
'jt.name as owner_job',
])
->addSelect('opp.stage_id', 'opp.stage_updated_at')
->addSelect(...[
'acc.name AS acc_name',
'acc.is_internal as acc_is_internal',
'opp.stage_updated_at',
'acc.crm_provider_id AS acc_provider_id',
'opp.crm_provider_id AS opp_provider_id',
])
->addSelect('rt.business_process_id AS pipeline_id')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'));
return $qb;
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws SocialAccountTokenInvalidException
*/
private function getCrmService(Team $team): ServiceInterface
{
$crmService = $this->providerRegistry->get($team->crm->provider);
$crmService->setConfiguration($team->crm);
if ($crmService instanceof UrlGeneratorInterface) {
$crmService->setCrmUrlGenerator($team->crm);
}
return $crmService;
}
/**
*
* @return Generator<DealData>
*/
public function getForecastData(DealsFilter $filter): Generator
{
$opportunities = DB::query()
->select([
'o.value',
'o.close_date',
'o.currency_code',
'o.is_won',
'o.is_closed',
'o.probability',
'o.forecast_category',
])
->from('opportunities', 'o')
->join('users', 'users.id', '=', 'o.user_id')
->join('groups', 'groups.id', '=', 'users.group_id')
->where('users.team_id', $filter->getTeam()->getId())
->where('o.close_date', '>=', $filter->getStartDate())
->where('o.close_date', '<=', $filter->getEndDate())
->where('o.currency_code', $filter->getCurrency())
->where('o.deleted_at', '=', null)
;
$userUuidList = $filter->getUserUuidList();
if (! empty($userUuidList)) {
$userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);
$opportunities->whereIn('users.uuid', $userUuidList);
}
$groupUuidList = $filter->getGroupUuidList();
if (! empty($groupUuidList)) {
$groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);
$opportunities->whereIn('groups.uuid', $groupUuidList);
}
foreach ($opportunities->cursor() as $row) {
yield new DealData(
(float) $row->value,
$row->close_date,
! empty($row->is_won),
! empty($row->is_closed),
$row->probability ?: 0,
$row->forecast_category ?: '',
);
}
}
public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection
{
return $user->subscriptionSets()
->where(static function (Eloquent\Builder $query): void {
$query
->whereNull('expired_at')
->orWhere('expired_at', '>=', now());
})
->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {
$join
->on('subscription_set_id', '=', 'activity_subscription_sets.id');
$join
->where('followable_type', Models\Activity\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)
->whereIn('followable_id', $opportunityIds);
})
->pluck('followable_id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
73358
|
|
73329
|
NULL
|
0
|
2026-04-23T07:21:06.986975+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776928866986_m1.jpg...
|
Slack
|
Stoyan Tomov (DM) - Jiminny Inc - 2 new items - Sl Stoyan Tomov (DM) - Jiminny Inc - 2 new items - Slack...
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
c-learning-people
confusion-clinic
curiosity_lab
deal-insights-dev
engineering
frontend
general
infra-changes
jiminny-bg
people-with-copilot-licences
people-with-zoom-phone-licences
platform-team
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Stoyan Tomov
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Nikolay Yankov
Petko Kashinski
Aneliya Angelova
Nikolay Nikolov
Mario Georgiev
Todor Stamatov
Gabriela Dureva
Vasil Vasilev
Galya Dimitrova
Stefka Stoyanova
Stoyan Tanev
Nikolay Ivanov
Toast
Jira Cloud
Messages
Messages
Add canvas
Add canvas
Files
Files
More
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Yesterday at 5:55:35 PM
5:55
също като други, имаш само един контакт, няма отговор, няма нищо ново някакво време …
Stoyan Tomov
Yesterday at 5:57:23 PM
5:57 PM
Еми значи трябва нещо да се промени, или от deal risk да се махне това да не може някой да го настрои да гледа за бъдещи срещи, или просто да се включват и scheduled конференции в таймлайна
Lukas Kovalik
Yesterday at 5:59:56 PM
5:59 PM
ами не знам, ровичках се в тях и може би за това ми са поясни
Yesterday at 6:00:29 PM
6:00
но да питай Галя ако иска да сменява
Yesterday at 6:01:43 PM
6:01
deal risk трябва да ти каже че сделка е под риск, ако са включени scheduled ти няма как да го отвориш и да виждаш, то трябва да е като playback
Jump to date
Stoyan Tomov
Today at 9:55:24 AM
9:55 AM
добро утро
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 9:56:16 AM
9:56
все още ли гледаме за нови сделки в хъбспот на 30 минути или и това е с webhooks?
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 10:12:46 AM
10:12 AM
добро утро, ами май е вече на 5 мин от няколко дни
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Stoyan Tomov
Today at 10:14:41 AM
10:14 AM
то това май важи и за контакти и акаунти или се бъркам?
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 10:15:02 AM
10:15 AM
да, sync objects
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
New
Stoyan Tomov
Today at 10:15:28 AM
10:15 AM
супер добре
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 10:15:48 AM
10:15
опитвах се да намеря инфо в confluence ама май не е писано там за синковете
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 10:21:03 AM
10:21 AM
от скоро е, Ники Николов го е правил тази седмица
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Conversation marked as unread from this message
Channel...
|
[{"role":"AXPopUpButton","text [{"role":"AXPopUpButton","text":"Switch workspaces… (Jiminny Inc) Has new messages","depth":14,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"Home","depth":14,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Home","depth":16,"role_description":"text"},{"role":"AXRadioButton","text":"DMs","depth":14,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"DMs","depth":16,"role_description":"text"},{"role":"AXRadioButton","text":"Activity","depth":14,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Activity","depth":16,"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":14,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":16,"role_description":"text"},{"role":"AXRadioButton","text":"Later","depth":14,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Later","depth":16,"role_description":"text"},{"role":"AXRadioButton","text":"More…","depth":14,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More","depth":16,"role_description":"text"},{"role":"AXStaticText","text":"Unreads","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Threads","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Huddles","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Drafts & sent","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Directories","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"jiminny-x-integration-app","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"platform-inner-team","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"ai-chapter","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"alerts","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"backend","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"c-learning-people","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"confusion-clinic","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"curiosity_lab","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"deal-insights-dev","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"engineering","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"frontend","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"general","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"infra-changes","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"jiminny-bg","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"people-with-copilot-licences","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"people-with-zoom-phone-licences","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"platform-team","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"platform-tickets","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"product_launches","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"random","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"releases","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"sofia-office","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"support","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"thank-yous","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"the_people_of_jiminny","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Stoyan Tomov","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":23,"role_description":"text"},{"role":"AXStaticText","text":",","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Yankov","depth":23,"role_description":"text"},{"role":"AXStaticText","text":",","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Steliyan Georgiev","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Yankov","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Petko Kashinski","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Nikolov","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Mario Georgiev","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Todor Stamatov","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Gabriela Dureva","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Vasil Vasilev","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Galya Dimitrova","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Stefka Stoyanova","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Stoyan Tanev","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Ivanov","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Toast","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Jira Cloud","depth":23,"role_description":"text"},{"role":"AXRadioButton","text":"Messages","depth":18,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Messages","depth":20,"role_description":"text"},{"role":"AXRadioButton","text":"Add canvas","depth":19,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Add canvas","depth":21,"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":18,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":20,"role_description":"text"},{"role":"AXRadioButton","text":"More","depth":19,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Add and Edit Channel Tabs","depth":18,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Canvas","depth":18,"role_description":"text"},{"role":"AXStaticText","text":"List","depth":18,"role_description":"text"},{"role":"AXStaticText","text":"Folder","depth":18,"role_description":"text"},{"role":"AXPopUpButton","text":"Jump to date","depth":24,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Yesterday at 5:55:35 PM","depth":26,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5:55","depth":27,"role_description":"text"},{"role":"AXStaticText","text":"също като други, имаш само един контакт, няма отговор, няма нищо ново някакво време …","depth":26,"role_description":"text"},{"role":"AXButton","text":"Stoyan Tomov","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":25,"role_description":"text"},{"role":"AXLink","text":"Yesterday at 5:57:23 PM","depth":25,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5:57 PM","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"Еми значи трябва нещо да се промени, или от deal risk да се махне това да не може някой да го настрои да гледа за бъдещи срещи, или просто да се включват и scheduled конференции в таймлайна","depth":26,"role_description":"text"},{"role":"AXButton","text":"Lukas Kovalik","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":25,"role_description":"text"},{"role":"AXLink","text":"Yesterday at 5:59:56 PM","depth":25,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5:59 PM","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"ами не знам, ровичках се в тях и може би за това ми са поясни","depth":26,"role_description":"text"},{"role":"AXLink","text":"Yesterday at 6:00:29 PM","depth":26,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6:00","depth":27,"role_description":"text"},{"role":"AXStaticText","text":"но да питай Галя ако иска да сменява","depth":26,"role_description":"text"},{"role":"AXLink","text":"Yesterday at 6:01:43 PM","depth":26,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6:01","depth":27,"role_description":"text"},{"role":"AXStaticText","text":"deal risk трябва да ти каже че сделка е под риск, ако са включени scheduled ти няма как да го отвориш и да виждаш, то трябва да е като playback","depth":26,"role_description":"text"},{"role":"AXPopUpButton","text":"Jump to date","depth":24,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Stoyan Tomov","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":25,"role_description":"text"},{"role":"AXLink","text":"Today at 9:55:24 AM","depth":25,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9:55 AM","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"добро утро","depth":26,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":27,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 9:56:16 AM","depth":26,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9:56","depth":27,"role_description":"text"},{"role":"AXStaticText","text":"все още ли гледаме за нови сделки в хъбспот на 30 минути или и това е с webhooks?","depth":26,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":27,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Lukas Kovalik","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":25,"role_description":"text"},{"role":"AXLink","text":"Today at 10:12:46 AM","depth":25,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:12 AM","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"добро утро, ами май е вече на 5 мин от няколко дни","depth":26,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":27,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Stoyan Tomov","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":25,"role_description":"text"},{"role":"AXLink","text":"Today at 10:14:41 AM","depth":25,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:14 AM","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"то това май важи и за контакти и акаунти или се бъркам?","depth":26,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":27,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Lukas Kovalik","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":25,"role_description":"text"},{"role":"AXLink","text":"Today at 10:15:02 AM","depth":25,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:15 AM","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"да, sync objects","depth":26,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":27,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"New","depth":23,"role_description":"text"},{"role":"AXButton","text":"Stoyan Tomov","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":25,"role_description":"text"},{"role":"AXLink","text":"Today at 10:15:28 AM","depth":25,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:15 AM","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"супер добре","depth":26,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":27,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 10:15:48 AM","depth":26,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:15","depth":27,"role_description":"text"},{"role":"AXStaticText","text":"опитвах се да намеря инфо в confluence ама май не е писано там за синковете","depth":26,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":27,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Lukas Kovalik","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":25,"role_description":"text"},{"role":"AXLink","text":"Today at 10:21:03 AM","depth":25,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:21 AM","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"от скоро е, Ники Николов го е правил тази седмица","depth":26,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":27,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"","depth":24,"value":"","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Conversation marked as unread from this message","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"Channel","depth":11,"role_description":"text"}]...
|
5913181592110813310
|
-1424943043252680620
|
visual_change
|
hybrid
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
c-learning-people
confusion-clinic
curiosity_lab
deal-insights-dev
engineering
frontend
general
infra-changes
jiminny-bg
people-with-copilot-licences
people-with-zoom-phone-licences
platform-team
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Stoyan Tomov
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Nikolay Yankov
Petko Kashinski
Aneliya Angelova
Nikolay Nikolov
Mario Georgiev
Todor Stamatov
Gabriela Dureva
Vasil Vasilev
Galya Dimitrova
Stefka Stoyanova
Stoyan Tanev
Nikolay Ivanov
Toast
Jira Cloud
Messages
Messages
Add canvas
Add canvas
Files
Files
More
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Yesterday at 5:55:35 PM
5:55
също като други, имаш само един контакт, няма отговор, няма нищо ново някакво време …
Stoyan Tomov
Yesterday at 5:57:23 PM
5:57 PM
Еми значи трябва нещо да се промени, или от deal risk да се махне това да не може някой да го настрои да гледа за бъдещи срещи, или просто да се включват и scheduled конференции в таймлайна
Lukas Kovalik
Yesterday at 5:59:56 PM
5:59 PM
ами не знам, ровичках се в тях и може би за това ми са поясни
Yesterday at 6:00:29 PM
6:00
но да питай Галя ако иска да сменява
Yesterday at 6:01:43 PM
6:01
deal risk трябва да ти каже че сделка е под риск, ако са включени scheduled ти няма как да го отвориш и да виждаш, то трябва да е като playback
Jump to date
Stoyan Tomov
Today at 9:55:24 AM
9:55 AM
добро утро
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 9:56:16 AM
9:56
все още ли гледаме за нови сделки в хъбспот на 30 минути или и това е с webhooks?
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 10:12:46 AM
10:12 AM
добро утро, ами май е вече на 5 мин от няколко дни
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Stoyan Tomov
Today at 10:14:41 AM
10:14 AM
то това май важи и за контакти и акаунти или се бъркам?
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 10:15:02 AM
10:15 AM
да, sync objects
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
New
Stoyan Tomov
Today at 10:15:28 AM
10:15 AM
супер добре
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 10:15:48 AM
10:15
опитвах се да намеря инфо в confluence ама май не е писано там за синковете
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 10:21:03 AM
10:21 AM
от скоро е, Ники Николов го е правил тази седмица
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Conversation marked as unread from this message
Channel
FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp# Userpilot Introducti... in 1h 9 mlБГ100% L28 • Thu 23 Apr 10:21:09meet.google.com/agt-teir-cwt?authuser=lukas.kovalik%40jiminny.comNikolay Yankov (Presenting)EdkM Inbox (0П1 ОрeQ Fiter files.• вй эррінт• # ControB UserB UserB jm8• Thu 23 Apr 10:21$23® sD useĐ Sidee)useĐ mainj:E routes8aptрl8 web.p'(1 Service-DeQNreports:F Project Pho0 Cost | Clauapp.devä3 Pipelines -Q Searche inslGalya DuDirect messages~Q Find a DMAneliya Angelova,5 mins 08NuwKowskcltyGeorgievтаитьла на репорта и boa sourcaSteliyan Georgiey. може би ще имаш збелеком поLukas Kovalik|e counite момонта со nomuawwer wewyintaro40 minsYou:2. Vor KyIlian KyuchukovYetwetihGalya DimitrovaаДо ако е лесно го сложи и там, ако еKowrobo chonno astocsletrsdNikolay Ivanove you, aлaш, сuuoe с наморилetkerovanoy2. Galya Dimitrova $Add canvasGalya Dimitrova 13.28O FilesQ Search messages in this channel (XF)Nikolay Yankov 1235иначе за това кога показваме страницата в момента проверката е следната:(Feature Flag AUTOMATED_REPORTS is ON ANO user-a has generated reports)(Feature Flug ASK_JIMINNY_REPORTS is ON AND user is ADMIN or Manager)Galya Dimitrova• IF (Feature Flug ASKC_JIMINNY_REPORTS iS ON AXO user is AOMIN or Manager)IF user has reports - then show the pageelse if user doesni't have reports - then show the new marketing page for AJ PanoramaELSE IF(Feature Flag AUTOMATED_REPORTS is ON)• if user-a has generated reports - then show the pageelse if user doesn't have reports AND user is ADMIN or Manager - then show the new marketing page for Exec ReportsELSEif user is user is ADMIN or Manager - then show the new marketing page for Exec Reports (edited)нещо такова трябва да стане като всче ти ще си нагодиш в кода как ще с най добреи на двете моста кансто има проверка за роля нама профлки да се пюльва нових пермишый конто добавиатаNikolay Yankow 13mдобретова условие отлада ли за analyst?B IMessage Galya Dimitrova = In a meeting • Google Calendara о+83 Jminny MiViewedNikolay YankovAneliya AngelovaLukas Kovalik10:21 AM | Daily - Platform• 35:28...
|
NULL
|
|
73324
|
NULL
|
0
|
2026-04-23T07:20:40.956878+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776928840956_m2.jpg...
|
Slack
|
Stoyan Tomov (DM) - Jiminny Inc - 2 new items - Sl Stoyan Tomov (DM) - Jiminny Inc - 2 new items - Slack...
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
c-learning-people
confusion-clinic
curiosity_lab
deal-insights-dev
engineering
frontend
general
infra-changes
jiminny-bg
people-with-copilot-licences
people-with-zoom-phone-licences
platform-team
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Stoyan Tomov
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Nikolay Yankov
Petko Kashinski
Aneliya Angelova
Nikolay Nikolov
Mario Georgiev
Todor Stamatov
Gabriela Dureva
Vasil Vasilev
Galya Dimitrova
Stefka Stoyanova
Stoyan Tanev
Nikolay Ivanov
Toast
Jira Cloud
Messages
Messages
Add canvas
Add canvas
Files
Files
More
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Lukas Kovalik
Yesterday at 5:54:29 PM
5:54 PM
това беше на Том изисване, трябва да ти каже че нямаш среща планирана и съответно сделка умира
Yesterday at 5:55:35 PM
5:55
също като други, имаш само един контакт, няма отговор, няма нищо ново някакво време …
Stoyan Tomov
Yesterday at 5:57:23 PM
5:57 PM
Еми значи трябва нещо да се промени, или от deal risk да се махне това да не може някой да го настрои да гледа за бъдещи срещи, или просто да се включват и scheduled конференции в таймлайна
Lukas Kovalik
Yesterday at 5:59:56 PM
5:59 PM
ами не знам, ровичках се в тях и може би за това ми са поясни
Yesterday at 6:00:29 PM
6:00
но да питай Галя ако иска да сменява
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Yesterday at 6:01:43 PM
6:01
deal risk трябва да ти каже че сделка е под риск, ако са включени scheduled ти няма как да го отвориш и да виждаш, то трябва да е като playback
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Jump to date
Stoyan Tomov
Today at 9:55:24 AM
9:55 AM
добро утро
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 9:56:16 AM
9:56
все още ли гледаме за нови сделки в хъбспот на 30 минути или и това е с webhooks?
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 10:12:46 AM
10:12 AM
добро утро, ами май е вече на 5 мин от няколко дни
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Stoyan Tomov
Today at 10:14:41 AM
10:14 AM
то това май важи и за контакти и акаунти или се бъркам?
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 10:15:02 AM
10:15 AM
да, sync objects
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
New
Stoyan Tomov
Today at 10:15:28 AM
10:15 AM
супер добре
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 10:15:48 AM
10:15
опитвах се да намеря инфо в confluence ама май не е писано там за синковете
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Conversation marked as unread from this message
Channel...
|
[{"role":"AXPopUpButton","text [{"role":"AXPopUpButton","text":"Switch workspaces… (Jiminny Inc) Has new messages","depth":14,"bounds":{"left":0.0056515955,"top":0.058260176,"width":0.011968086,"height":0.028731046},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"Home","depth":14,"bounds":{"left":0.0029920214,"top":0.10055866,"width":0.017287234,"height":0.054269753},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Home","depth":16,"bounds":{"left":0.0066489363,"top":0.13806863,"width":0.009973404,"height":0.0103751},"role_description":"text"},{"role":"AXRadioButton","text":"DMs","depth":14,"bounds":{"left":0.0029920214,"top":0.15482841,"width":0.017287234,"height":0.054269753},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"DMs","depth":16,"bounds":{"left":0.0076462766,"top":0.19233839,"width":0.007978723,"height":0.0103751},"role_description":"text"},{"role":"AXRadioButton","text":"Activity","depth":14,"bounds":{"left":0.0029920214,"top":0.20909816,"width":0.017287234,"height":0.054269753},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Activity","depth":16,"bounds":{"left":0.004986702,"top":0.24660814,"width":0.012965426,"height":0.0103751},"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":14,"bounds":{"left":0.0029920214,"top":0.26336792,"width":0.017287234,"height":0.054269753},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":16,"bounds":{"left":0.0076462766,"top":0.3008779,"width":0.0076462766,"height":0.0103751},"role_description":"text"},{"role":"AXRadioButton","text":"Later","depth":14,"bounds":{"left":0.0029920214,"top":0.31763768,"width":0.017287234,"height":0.054269753},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Later","depth":16,"bounds":{"left":0.00731383,"top":0.35514766,"width":0.008643617,"height":0.0103751},"role_description":"text"},{"role":"AXRadioButton","text":"More…","depth":14,"bounds":{"left":0.0029920214,"top":0.3719074,"width":0.017287234,"height":0.054269753},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More","depth":16,"bounds":{"left":0.006981383,"top":0.4094174,"width":0.008976064,"height":0.0103751},"role_description":"text"},{"role":"AXStaticText","text":"Unreads","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Threads","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Huddles","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Drafts & sent","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Directories","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"jiminny-x-integration-app","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"platform-inner-team","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"ai-chapter","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"alerts","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"backend","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"c-learning-people","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"confusion-clinic","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"curiosity_lab","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"deal-insights-dev","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"engineering","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"frontend","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"general","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"infra-changes","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"jiminny-bg","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"people-with-copilot-licences","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"people-with-zoom-phone-licences","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"platform-team","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"platform-tickets","depth":23,"bounds":{"left":0.042220745,"top":0.09177973,"width":0.034906916,"height":0.003990423},"role_description":"text"},{"role":"AXStaticText","text":"product_launches","depth":23,"bounds":{"left":0.042220745,"top":0.103751,"width":0.03856383,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"random","depth":23,"bounds":{"left":0.042220745,"top":0.12609737,"width":0.01662234,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"releases","depth":23,"bounds":{"left":0.042220745,"top":0.14844373,"width":0.01761968,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"sofia-office","depth":23,"bounds":{"left":0.042220745,"top":0.1707901,"width":0.024268618,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"support","depth":23,"bounds":{"left":0.042220745,"top":0.19313647,"width":0.016954787,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"thank-yous","depth":23,"bounds":{"left":0.042220745,"top":0.21548285,"width":0.024268618,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"the_people_of_jiminny","depth":23,"bounds":{"left":0.042220745,"top":0.23782921,"width":0.04488032,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Stoyan Tomov","depth":23,"bounds":{"left":0.042220745,"top":0.2905028,"width":0.030585106,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":23,"bounds":{"left":0.042220745,"top":0.31284916,"width":0.03756649,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":",","depth":23,"bounds":{"left":0.07945479,"top":0.31284916,"width":0.0063164895,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Yankov","depth":23,"bounds":{"left":0.08211436,"top":0.31284916,"width":0.014295213,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":",","depth":23,"bounds":{"left":0.09607713,"top":0.33040702,"width":0.0003324468,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Steliyan Georgiev","depth":23,"bounds":{"left":0.09607713,"top":0.33040702,"width":0.0003324468,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Yankov","depth":23,"bounds":{"left":0.042220745,"top":0.33519554,"width":0.032912236,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Petko Kashinski","depth":23,"bounds":{"left":0.042220745,"top":0.3575419,"width":0.034242023,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":23,"bounds":{"left":0.042220745,"top":0.37988827,"width":0.03756649,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Nikolov","depth":23,"bounds":{"left":0.042220745,"top":0.40223464,"width":0.034242023,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Mario Georgiev","depth":23,"bounds":{"left":0.042220745,"top":0.424581,"width":0.033909574,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Todor Stamatov","depth":23,"bounds":{"left":0.042220745,"top":0.44692737,"width":0.034242023,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Gabriela Dureva","depth":23,"bounds":{"left":0.042220745,"top":0.46927375,"width":0.03523936,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Vasil Vasilev","depth":23,"bounds":{"left":0.042220745,"top":0.49162012,"width":0.026263298,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Galya Dimitrova","depth":23,"bounds":{"left":0.042220745,"top":0.5139665,"width":0.034906916,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Stefka Stoyanova","depth":23,"bounds":{"left":0.042220745,"top":0.5363129,"width":0.03756649,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Stoyan Tanev","depth":23,"bounds":{"left":0.042220745,"top":0.5586592,"width":0.028922873,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Ivanov","depth":23,"bounds":{"left":0.042220745,"top":0.5810056,"width":0.031914894,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Toast","depth":23,"bounds":{"left":0.042220745,"top":0.63367915,"width":0.011968086,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Jira Cloud","depth":23,"bounds":{"left":0.042220745,"top":0.6560255,"width":0.021609042,"height":0.014365523},"role_description":"text"},{"role":"AXRadioButton","text":"Messages","depth":18,"bounds":{"left":0.10206117,"top":0.09177973,"width":0.030585106,"height":0.030327214},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Messages","depth":20,"bounds":{"left":0.111369684,"top":0.10055866,"width":0.01861702,"height":0.012769354},"role_description":"text"},{"role":"AXRadioButton","text":"Add canvas","depth":19,"bounds":{"left":0.13397606,"top":0.09177973,"width":0.033909574,"height":0.030327214},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Add canvas","depth":21,"bounds":{"left":0.14328457,"top":0.10055866,"width":0.021941489,"height":0.012769354},"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":18,"bounds":{"left":0.16921543,"top":0.09177973,"width":0.020944148,"height":0.030327214},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":20,"bounds":{"left":0.17852394,"top":0.10055866,"width":0.008976064,"height":0.012769354},"role_description":"text"},{"role":"AXRadioButton","text":"More","depth":19,"bounds":{"left":0.19115691,"top":0.09177973,"width":0.020279255,"height":0.030327214},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Add and Edit Channel Tabs","depth":18,"bounds":{"left":0.21143617,"top":0.09177973,"width":0.010638298,"height":0.030327214},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Canvas","depth":18,"bounds":{"left":0.096409574,"top":0.0518755,"width":0.015625,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"List","depth":18,"bounds":{"left":0.096409574,"top":0.0518755,"width":0.0076462766,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Folder","depth":18,"bounds":{"left":0.096409574,"top":0.0518755,"width":0.013962766,"height":0.0007980846},"role_description":"text"},{"role":"AXPopUpButton","text":"Jump to date","depth":23,"bounds":{"left":0.14660904,"top":0.12689546,"width":0.032579787,"height":0.022346368},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"role_description":"text"},{"role":"AXLink","text":"Yesterday at 5:54:29 PM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5:54 PM","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"това беше на Том изисване, трябва да ти каже че нямаш среща планирана и съответно сделка умира","depth":25,"role_description":"text"},{"role":"AXLink","text":"Yesterday at 5:55:35 PM","depth":25,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5:55","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"също като други, имаш само един контакт, няма отговор, няма нищо ново някакво време …","depth":25,"role_description":"text"},{"role":"AXButton","text":"Stoyan Tomov","depth":24,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"role_description":"text"},{"role":"AXLink","text":"Yesterday at 5:57:23 PM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5:57 PM","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"Еми значи трябва нещо да се промени, или от deal risk да се махне това да не може някой да го настрои да гледа за бъдещи срещи, или просто да се включват и scheduled конференции в таймлайна","depth":25,"role_description":"text"},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"role_description":"text"},{"role":"AXLink","text":"Yesterday at 5:59:56 PM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5:59 PM","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"ами не знам, ровичках се в тях и може би за това ми са поясни","depth":25,"bounds":{"left":0.11801862,"top":0.11572227,"width":0.10106383,"height":0.0103751},"role_description":"text"},{"role":"AXLink","text":"Yesterday at 6:00:29 PM","depth":25,"bounds":{"left":0.107380316,"top":0.13806863,"width":0.007978723,"height":0.011173184},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6:00","depth":26,"bounds":{"left":0.107380316,"top":0.13806863,"width":0.007978723,"height":0.011173184},"role_description":"text"},{"role":"AXStaticText","text":"но да питай Галя ако иска да сменява","depth":25,"bounds":{"left":0.11801862,"top":0.13567439,"width":0.08577128,"height":0.014365523},"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"bounds":{"left":0.13730054,"top":0.11572227,"width":0.010638298,"height":0.0207502},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"bounds":{"left":0.14793883,"top":0.11572227,"width":0.010638298,"height":0.0207502},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"bounds":{"left":0.15857713,"top":0.11572227,"width":0.010638298,"height":0.0207502},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"bounds":{"left":0.16921543,"top":0.11572227,"width":0.010638298,"height":0.0207502},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"bounds":{"left":0.17985372,"top":0.11572227,"width":0.010638298,"height":0.0207502},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"bounds":{"left":0.22340426,"top":0.11572227,"width":0.0003324468,"height":0.0207502},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"bounds":{"left":0.22340426,"top":0.11572227,"width":0.0003324468,"height":0.0207502},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"bounds":{"left":0.22340426,"top":0.11572227,"width":0.0003324468,"height":0.0207502},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Yesterday at 6:01:43 PM","depth":25,"bounds":{"left":0.107380316,"top":0.16201118,"width":0.007978723,"height":0.011173184},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6:01","depth":26,"bounds":{"left":0.107380316,"top":0.16201118,"width":0.007978723,"height":0.011173184},"role_description":"text"},{"role":"AXStaticText","text":"deal risk трябва да ти каже че сделка е под риск, ако са включени scheduled ти няма как да го отвориш и да виждаш, то трябва да е като playback","depth":25,"bounds":{"left":0.11801862,"top":0.15961692,"width":0.10239362,"height":0.06703911},"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"bounds":{"left":0.13730054,"top":0.1348763,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"bounds":{"left":0.14793883,"top":0.1348763,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"bounds":{"left":0.15857713,"top":0.1348763,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"bounds":{"left":0.16921543,"top":0.1348763,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"bounds":{"left":0.17985372,"top":0.1348763,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"bounds":{"left":0.22340426,"top":0.1348763,"width":0.0003324468,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"bounds":{"left":0.22340426,"top":0.1348763,"width":0.0003324468,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"bounds":{"left":0.22340426,"top":0.1348763,"width":0.0003324468,"height":0.025538707},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Jump to date","depth":23,"bounds":{"left":0.15026596,"top":0.24261771,"width":0.025265958,"height":0.022346368},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Stoyan Tomov","depth":24,"bounds":{"left":0.11801862,"top":0.273743,"width":0.03158245,"height":0.017557861},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.14926861,"top":0.2753392,"width":0.0026595744,"height":0.014365523},"role_description":"text"},{"role":"AXLink","text":"Today at 9:55:24 AM","depth":24,"bounds":{"left":0.15192819,"top":0.27773345,"width":0.015292553,"height":0.011173184},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9:55 AM","depth":25,"bounds":{"left":0.15192819,"top":0.27773345,"width":0.015292553,"height":0.011173184},"role_description":"text"},{"role":"AXStaticText","text":"добро утро","depth":25,"bounds":{"left":0.11801862,"top":0.29289705,"width":0.026263298,"height":0.014365523},"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"bounds":{"left":0.13730054,"top":0.2601756,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"bounds":{"left":0.14793883,"top":0.2601756,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"bounds":{"left":0.15857713,"top":0.2601756,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"bounds":{"left":0.16921543,"top":0.2601756,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"bounds":{"left":0.17985372,"top":0.2601756,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"bounds":{"left":0.22340426,"top":0.2601756,"width":0.0003324468,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"bounds":{"left":0.22340426,"top":0.2601756,"width":0.0003324468,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"bounds":{"left":0.22340426,"top":0.2601756,"width":0.0003324468,"height":0.025538707},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 9:56:16 AM","depth":25,"bounds":{"left":0.107380316,"top":0.31923383,"width":0.007978723,"height":0.011173184},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9:56","depth":26,"bounds":{"left":0.107380316,"top":0.31923383,"width":0.007978723,"height":0.011173184},"role_description":"text"},{"role":"AXStaticText","text":"все още ли гледаме за нови сделки в хъбспот на 30 минути или и това е с webhooks?","depth":25,"bounds":{"left":0.11801862,"top":0.31683958,"width":0.08543883,"height":0.049481247},"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"bounds":{"left":0.13730054,"top":0.29209897,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"bounds":{"left":0.14793883,"top":0.29209897,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"bounds":{"left":0.15857713,"top":0.29209897,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"bounds":{"left":0.16921543,"top":0.29209897,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"bounds":{"left":0.17985372,"top":0.29209897,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"bounds":{"left":0.22340426,"top":0.29209897,"width":0.0003324468,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"bounds":{"left":0.22340426,"top":0.29209897,"width":0.0003324468,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"bounds":{"left":0.22340426,"top":0.29209897,"width":0.0003324468,"height":0.025538707},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"bounds":{"left":0.11801862,"top":0.37430167,"width":0.030917553,"height":0.017557861},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.14860372,"top":0.37589785,"width":0.0029920214,"height":0.014365523},"role_description":"text"},{"role":"AXLink","text":"Today at 10:12:46 AM","depth":24,"bounds":{"left":0.1512633,"top":0.3782921,"width":0.01761968,"height":0.011173184},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:12 AM","depth":25,"bounds":{"left":0.1512633,"top":0.3782921,"width":0.01761968,"height":0.011173184},"role_description":"text"},{"role":"AXStaticText","text":"добро утро, ами май е вече на 5 мин от няколко дни","depth":25,"bounds":{"left":0.11801862,"top":0.3934557,"width":0.09242021,"height":0.031923383},"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"bounds":{"left":0.13730054,"top":0.36073422,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"bounds":{"left":0.14793883,"top":0.36073422,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"bounds":{"left":0.15857713,"top":0.36073422,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"bounds":{"left":0.16921543,"top":0.36073422,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"bounds":{"left":0.17985372,"top":0.36073422,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"bounds":{"left":0.22340426,"top":0.36073422,"width":0.0003324468,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"bounds":{"left":0.22340426,"top":0.36073422,"width":0.0003324468,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"bounds":{"left":0.22340426,"top":0.36073422,"width":0.0003324468,"height":0.025538707},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Stoyan Tomov","depth":24,"bounds":{"left":0.11801862,"top":0.43335995,"width":0.03158245,"height":0.017557861},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.14926861,"top":0.4349561,"width":0.0026595744,"height":0.014365523},"role_description":"text"},{"role":"AXLink","text":"Today at 10:14:41 AM","depth":24,"bounds":{"left":0.15192819,"top":0.43735036,"width":0.01761968,"height":0.011173184},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:14 AM","depth":25,"bounds":{"left":0.15192819,"top":0.43735036,"width":0.01761968,"height":0.011173184},"role_description":"text"},{"role":"AXStaticText","text":"то това май важи и за контакти и акаунти или се бъркам?","depth":25,"bounds":{"left":0.11801862,"top":0.45251396,"width":0.09607713,"height":0.031923383},"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"bounds":{"left":0.13730054,"top":0.4197925,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"bounds":{"left":0.14793883,"top":0.4197925,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"bounds":{"left":0.15857713,"top":0.4197925,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"bounds":{"left":0.16921543,"top":0.4197925,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"bounds":{"left":0.17985372,"top":0.4197925,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"bounds":{"left":0.22340426,"top":0.4197925,"width":0.0003324468,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"bounds":{"left":0.22340426,"top":0.4197925,"width":0.0003324468,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"bounds":{"left":0.22340426,"top":0.4197925,"width":0.0003324468,"height":0.025538707},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"bounds":{"left":0.11801862,"top":0.4924182,"width":0.030917553,"height":0.017557861},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.14860372,"top":0.49401435,"width":0.0029920214,"height":0.014365523},"role_description":"text"},{"role":"AXLink","text":"Today at 10:15:02 AM","depth":24,"bounds":{"left":0.1512633,"top":0.4964086,"width":0.01761968,"height":0.011173184},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:15 AM","depth":25,"bounds":{"left":0.1512633,"top":0.4964086,"width":0.01761968,"height":0.011173184},"role_description":"text"},{"role":"AXStaticText","text":"да, sync objects","depth":25,"bounds":{"left":0.11801862,"top":0.51157224,"width":0.034906916,"height":0.014365523},"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"bounds":{"left":0.13730054,"top":0.47885075,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"bounds":{"left":0.14793883,"top":0.47885075,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"bounds":{"left":0.15857713,"top":0.47885075,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"bounds":{"left":0.16921543,"top":0.47885075,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"bounds":{"left":0.17985372,"top":0.47885075,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"bounds":{"left":0.22340426,"top":0.47885075,"width":0.0003324468,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"bounds":{"left":0.22340426,"top":0.47885075,"width":0.0003324468,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"bounds":{"left":0.22340426,"top":0.47885075,"width":0.0003324468,"height":0.025538707},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"New","depth":22,"bounds":{"left":0.21343085,"top":0.5235435,"width":0.00930851,"height":0.012769354},"role_description":"text"},{"role":"AXButton","text":"Stoyan Tomov","depth":24,"bounds":{"left":0.11801862,"top":0.5339186,"width":0.03158245,"height":0.017557861},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.14926861,"top":0.5355148,"width":0.0026595744,"height":0.014365523},"role_description":"text"},{"role":"AXLink","text":"Today at 10:15:28 AM","depth":24,"bounds":{"left":0.15192819,"top":0.53790903,"width":0.01761968,"height":0.011173184},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:15 AM","depth":25,"bounds":{"left":0.15192819,"top":0.53790903,"width":0.01761968,"height":0.011173184},"role_description":"text"},{"role":"AXStaticText","text":"супер добре","depth":25,"bounds":{"left":0.11801862,"top":0.55307263,"width":0.028590426,"height":0.014365523},"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"bounds":{"left":0.13730054,"top":0.5203512,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"bounds":{"left":0.14793883,"top":0.5203512,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"bounds":{"left":0.15857713,"top":0.5203512,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"bounds":{"left":0.16921543,"top":0.5203512,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"bounds":{"left":0.17985372,"top":0.5203512,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"bounds":{"left":0.22340426,"top":0.5203512,"width":0.0003324468,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"bounds":{"left":0.22340426,"top":0.5203512,"width":0.0003324468,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"bounds":{"left":0.22340426,"top":0.5203512,"width":0.0003324468,"height":0.025538707},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 10:15:48 AM","depth":25,"bounds":{"left":0.105053194,"top":0.5794094,"width":0.010305851,"height":0.011173184},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:15","depth":26,"bounds":{"left":0.105053194,"top":0.5794094,"width":0.010305851,"height":0.011173184},"role_description":"text"},{"role":"AXStaticText","text":"опитвах се да намеря инфо в confluence ама май не е писано там за синковете","depth":25,"bounds":{"left":0.11801862,"top":0.57701516,"width":0.10172872,"height":0.031923383},"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"bounds":{"left":0.13730054,"top":0.5522745,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"bounds":{"left":0.14793883,"top":0.5522745,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"bounds":{"left":0.15857713,"top":0.5522745,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"bounds":{"left":0.16921543,"top":0.5522745,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"bounds":{"left":0.17985372,"top":0.5522745,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"bounds":{"left":0.22340426,"top":0.5522745,"width":0.0003324468,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"bounds":{"left":0.22340426,"top":0.5522745,"width":0.0003324468,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"bounds":{"left":0.22340426,"top":0.5522745,"width":0.0003324468,"height":0.025538707},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"","depth":24,"bounds":{"left":0.10372341,"top":0.6272945,"width":0.118351065,"height":0.030327214},"value":"","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Conversation marked as unread from this message","depth":11,"bounds":{"left":0.0,"top":0.7126895,"width":0.02825798,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Channel","depth":11,"bounds":{"left":0.0,"top":0.7126895,"width":0.017287234,"height":0.0007980846},"role_description":"text"}]...
|
8315943079411805345
|
-1424943043250583468
|
click
|
hybrid
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
c-learning-people
confusion-clinic
curiosity_lab
deal-insights-dev
engineering
frontend
general
infra-changes
jiminny-bg
people-with-copilot-licences
people-with-zoom-phone-licences
platform-team
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Stoyan Tomov
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Nikolay Yankov
Petko Kashinski
Aneliya Angelova
Nikolay Nikolov
Mario Georgiev
Todor Stamatov
Gabriela Dureva
Vasil Vasilev
Galya Dimitrova
Stefka Stoyanova
Stoyan Tanev
Nikolay Ivanov
Toast
Jira Cloud
Messages
Messages
Add canvas
Add canvas
Files
Files
More
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Lukas Kovalik
Yesterday at 5:54:29 PM
5:54 PM
това беше на Том изисване, трябва да ти каже че нямаш среща планирана и съответно сделка умира
Yesterday at 5:55:35 PM
5:55
също като други, имаш само един контакт, няма отговор, няма нищо ново някакво време …
Stoyan Tomov
Yesterday at 5:57:23 PM
5:57 PM
Еми значи трябва нещо да се промени, или от deal risk да се махне това да не може някой да го настрои да гледа за бъдещи срещи, или просто да се включват и scheduled конференции в таймлайна
Lukas Kovalik
Yesterday at 5:59:56 PM
5:59 PM
ами не знам, ровичках се в тях и може би за това ми са поясни
Yesterday at 6:00:29 PM
6:00
но да питай Галя ако иска да сменява
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Yesterday at 6:01:43 PM
6:01
deal risk трябва да ти каже че сделка е под риск, ако са включени scheduled ти няма как да го отвориш и да виждаш, то трябва да е като playback
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Jump to date
Stoyan Tomov
Today at 9:55:24 AM
9:55 AM
добро утро
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 9:56:16 AM
9:56
все още ли гледаме за нови сделки в хъбспот на 30 минути или и това е с webhooks?
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 10:12:46 AM
10:12 AM
добро утро, ами май е вече на 5 мин от няколко дни
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Stoyan Tomov
Today at 10:14:41 AM
10:14 AM
то това май важи и за контакти и акаунти или се бъркам?
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 10:15:02 AM
10:15 AM
да, sync objects
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
New
Stoyan Tomov
Today at 10:15:28 AM
10:15 AM
супер добре
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 10:15:48 AM
10:15
опитвах се да намеря инфо в confluence ама май не е писано там за синковете
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Conversation marked as unread from this message
Channel
Home1889FilesLateMoreSlackVIewmistonWindowhelt@ Describe what you are looking forJiminny ...# product_launches# randomi releases# sofia-office# support# thank-yous# the people of jimi..A Direct messagesStoyan Tomov3 Aneliya Angelova, ...Nikolay Yankovf Petko Kashinski. Aneliya Angelovae. Nikolay Nikolov8 Mario GeorgievSe: Todor StamatovA Gabriela Dureva•. Vasil Vasilev. Galva Dimitrovan Stefka StoyanovaStoyan TanevNikolav Ivanov#:Apps® Toast" Jira CloudStoyan TomovMessagesAdd canvaUr FilesMorevно да питайда сменяваdeal risk трябва да ти каже че сделка е подписк. ако са включени scheduled ти няма как.да го отвориш и да виждаш, то трябва да еKaтo playbackStoyan Tomov 9:55 AMnоб0о УТрOвсе още ли гледаме за нови сделки вхъбспот на 30 минути или и това е сwehhaaks?uukas Kovallk 10-12 AMIдобро утро, ами май е вече на 5 мин отНЯКОЛКО ДНИStovan llomov 10-14AMто това май важи и за контакти и зкаvнти.или се біокам?Lukas Kovallk 10:15 AMna sune obiecteStovan Tomov 10:15 ANсvnen лобоеопитвах се ла намеря инфо в contllence амамай не е писано там за синковетеMessafe Stovan TomovFV faVsco.js°9 JY-20157-AJ-report-not-send-notificatProject= custom.log= laravel.log© EventServiceProvider.php© HubspotJournalServiceProvider.p© HubspotWebhookServiceProvide© JiminnyServiceProvider.php(c) PlanhatServiceProvider.png© ProphetHandlerServiceProvider.p© PusherServiceProvider.php© QueueLogServiceProvider.php© QueueStatsdServiceProvider.php© ResponseMacroServiceProvider.r© RouteServiceProvider.php© SsoServiceProvider.php© UtilServiceProvider.phpAskJminnykeporacuvilyservice.org© AutomatedReportsCommand.phpA SF [jiminny@localhost]• repori-not-gen.©) PlannatActivityListener.php# console PRODAutomatedreporiskepository.ono xpnp api vz.ohp29.07.25 NIkolov3.09.2523.10.25 Ivanov23.10.25 Ivanov23.10.25 Ivanov3.09,25vanov3.09.25 vanov3.09.25 Ivanovclass AutomatedreportsRepositorynuouc runction cetpacnateduserRevortsSquery->whereL1ke416 X781A© DealsRepository.php x"%5namex");& console (EU4console [STAGING)'page', Soage):-141=143A36 ^public function ge$qb = Activity::with(lc) ViewercuaroserviceProvider.ono>0 Queuev → Repositories>Ai> → AutoScorina› D Calendarverm© AccountRepository.php© ContactRepository.php© ContactRoleRepository.php© CrmConfigurationRepository.p© CrmEntityRepository.php© FieldDataRepository.php© FieldRepository.php© LayoutEntityRepository.php© LayoutRepository.php© LeadRepository.php© OpportunityRepository.php© ProfileRepository.php© RecordTypeFieldValuesReposi© StageRepository.php© SyncBatchRepository.php> C Geography© ActiveStreamsRepositorv.php© ActivityCommentRepository.php© ActivityLoaRepository.pho© [EMAIL]©[EMAIL]@ActivitvSearchFilterRenositorv.oh@ ActivitvShareRepositorv.ohr© ActivitvUoloadSettinaRenositorv.3 usaapopubLi3.09.25Ivanov3.09.25 Ivanov3.09.25Ivanov3.09.25Ivanov3.09.25 Ivanov3.09.25 Ivanov3.09.25 Ivanov3.09.25 Ivanov3.09.25 Ivanov3.09.25 Ivanov3.09.25 Ivanov19.09.25 Nikolov21.04.26 Kovalik21.04.26 Kovalik21.04.26 Kovalik21.04.26 Kovalik21.04.26 Kovalik21.04.26 Kovallk74.04.26 KovallkMethod \Jiminny\Repositories\AutomatedReportsRepository.countUserReports AutoaProiect Filesv© UserTransformer.php° UserPolicy.php@ CanAccessAiReportsTest.php 558& Sthis->automatedReportsRepositorv->countUserReports(Suser) > 0) <-swhene.Isontontainscoln: 'recipients->users', $user->getIdO):return sab->deto->at154|->countO:155 Gt > ic function aetStages(* Restrict a quenu on the autonated neponte table te renerte the given user is alior 180 C> io functtion getconfie›ic function getPipelir- expu1cit user recipients (reczpzents.users)members of anu of the report's qroups Ask Jiminnu reports)ate function cnpateni22921.04.26 Kovalik21.04.26 Kovalikprivate function applvlserAccessScope(Builder Squery. User Suser): voidpplies all applicable21.04.26 Kovalik21.04.26 KovalikSuserid = Suser->qetido:Saroupid = Suser->aetGroupido:return stringl]23421.04.26 Kovalik71.04.26 Kovallk21.04.26 Kovalik21.04.26 KovalikSauerylautomated nenonts team id' Susen->aetTeamTdonl, gee funation visitcoue297->where(function (Builder $q) use (SuserId, SgroupId): void ‹Sa->whene.IsonContainsd col21.04.26 Kovalik21.04.26 Kovalik21.04.26 Kovalik258,3ee funation nudratesti& (Sarountd 1== null)21OAD6 KovAlik(C) A'PromotRenositorv.oho@AckAnvthinaRenository.nhr21OAD6 KovAlil210426 Kovalil$q->orWhere(function (Builder $sub) use (SgroupId): void {Cenh-swhone & column•automated_reports.type', operator: Automatedpz 278-swhono.lconfontaine/ columi'automated_reports.groups', $grouparam intl] $dealids9 AutomatedReportsRepository.php210426 Kovolil@ CallimportRepository.php21.04.26 Kovalik(C) CoachinaFeedhackRenasitorv nhil21.04.26 Kovalikate Function FetchStad"userollot Introcuct.. In Th 10m100% S2• Thu 23 Apr 10:20:41+0 ..CascadeAutomated Reports LOusers (explicitelly). For other reports the groups are used for filtering and we should only show results if the user is inThouaht for 24glYou're right. The app lyUserAccessScope logic is actually correct for access control, but countUserReports doesn't match it.Here's the breakdown:Current annivllserAccecsScone (correct)Ask Jiminny: Access via groups OR recipients.users• Non-Ask Jiminny: Access via recipients.users onlycountUserReports (missing aroup access): Onlv checks recipients.users - undercounts for Ask Jiminny users with aroupedReportsRepository.php:271-281inction countliserRenorts(llser Suser)• intl-Id = $user->getId();upId = $user->getGroupId():Karirn AutonatedRenortResult::querv()→wherenas report,function ($q) use (Suser, SuserId, $groupld) {So->where"team_id', Suser->qetTeamido_swhere(function (sa)) use (Suserld. SarounTd) {if (SgroupId !== null) {$a2->orWhere function Ssub use (Saroupid)Scuh-swhere/Itvne' AutomatedRenortcService:-TYPF_ASK 1TMTNNY->whereJsonContains('groups'. SaroupId):>counto:Regarding the teamIds filter (lines 252-258): This works correctly as a content filter. When users pass ?team (]=A, they'rePotential edge case to verify: Non-Ask Jiminny reports with empty groups array won't appear when filtering by specific teamsThis is orobablv intended - if a report filters by "no arouns" it shouldn't show when user asks for "arouo A renorts"The transformer change at line 139 is not okay without this fix - it will show false for users who only have Ask Jiminny accessvia arouos.ral .Ask anvthina (*4L)→ CodeC° AdantivePo 4 spac...
|
73322
|
|
73297
|
NULL
|
0
|
2026-04-23T07:15:39.457692+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776928539457_m1.jpg...
|
Firefox
|
Meet - Daily - Platform — Work
|
True
|
meet.google.com/agt-teir-cwt?authuser=lukas.kovali meet.google.com/agt-teir-cwt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - Daily - Platform
Close tab
New Tab
Open Goo Meet - Daily - Platform
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
4
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Zoom in
Open in new window
Enter Full Screen
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Nikolay Yankov
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Aneliya Angelova
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Lukas Kovalik
Others might see more of your background. Click to view your full video.
10:15
AM
Daily - Platform
Daily - Platform
Audio settings
Turn off microphone
Video settings
Turn off camera
Nikolay Yankov is presenting
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Nikolay Ivanov has left the meeting...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - Daily - Platform","depth":4,"bounds":{"left":0.0,"top":0.072222225,"width":0.033680554,"height":0.045555554},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.0013888889,"top":0.072222225,"width":0.010416667,"height":0.016666668},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.005902778,"top":0.12,"width":0.022222223,"height":0.035555556},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.0,"top":0.7977778,"width":0.033680554,"height":0.043333333},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.0,"top":0.8411111,"width":0.033680554,"height":0.038333334},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.0,"top":0.8794444,"width":0.033680554,"height":0.03888889},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.0,"top":0.91833335,"width":0.033680554,"height":0.038333334},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0,"top":0.95666665,"width":0.033680554,"height":0.043333333},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Nikolay Yankov (Presenting)","depth":12,"bounds":{"left":0.07534722,"top":0.101111114,"width":0.124305554,"height":0.022222223},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Nikolay Yankov (Presenting)","depth":13,"bounds":{"left":0.07534722,"top":0.10222222,"width":0.124305554,"height":0.020555556},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"People","depth":15,"bounds":{"left":0.88645834,"top":0.08944444,"width":0.041319445,"height":0.04},"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":22,"bounds":{"left":0.9142361,"top":0.101111114,"width":0.0052083335,"height":0.017222222},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Take notes with Gemini","depth":14,"bounds":{"left":0.93333334,"top":0.08944444,"width":0.025,"height":0.04},"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Take notes with Gemini","depth":17,"bounds":{"left":0.9361111,"top":0.101111114,"width":0.06388891,"height":0.017222222},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Gemini","depth":22,"bounds":{"left":0.96666664,"top":0.101111114,"width":0.028125,"height":0.017222222},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Gemini","depth":21,"bounds":{"left":0.96458334,"top":0.090555556,"width":0.023611112,"height":0.037777778},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Pop out this video More screens are more fun. Play this video while you do other things.","depth":15,"bounds":{"left":0.5798611,"top":0.61,"width":0.14652778,"height":0.08888889},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pop out this video","depth":17,"bounds":{"left":0.7239583,"top":0.6244444,"width":0.08090278,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"More screens are more fun. Play this video while you do other things.","depth":16,"bounds":{"left":0.7017361,"top":0.6205556,"width":0.11076389,"height":0.05666667},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Zoom in","depth":13,"bounds":{"left":0.63090277,"top":0.78333336,"width":0.027777778,"height":0.044444446},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open in new window","depth":13,"bounds":{"left":0.6642361,"top":0.78333336,"width":0.027777778,"height":0.044444446},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Enter Full Screen","depth":13,"bounds":{"left":0.69756943,"top":0.78333336,"width":0.027777778,"height":0.044444446},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Pop out this video More screens are more fun. Play this video while you do other things.","depth":15,"bounds":{"left":0.8347222,"top":0.295,"width":0.14652778,"height":0.07722222},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pop out this video","depth":17,"bounds":{"left":0.97881943,"top":0.31,"width":0.02118057,"height":0.017777778},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"More screens are more fun. Play this video while you do other things.","depth":16,"bounds":{"left":0.9565972,"top":0.30555555,"width":0.04340279,"height":0.045},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Nikolay Yankov","depth":17,"bounds":{"left":0.753125,"top":0.375,"width":0.07673611,"height":0.022777777},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pop out this video More screens are more fun. Play this video while you do other things.","depth":15,"bounds":{"left":0.8347222,"top":0.5283333,"width":0.14652778,"height":0.07722222},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pop out this video","depth":17,"bounds":{"left":0.97881943,"top":0.54333335,"width":0.02118057,"height":0.017777778},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"More screens are more fun. Play this video while you do other things.","depth":16,"bounds":{"left":0.9565972,"top":0.5388889,"width":0.04340279,"height":0.045},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":17,"bounds":{"left":0.753125,"top":0.60833335,"width":0.088541664,"height":0.022777777},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pop out this video More screens are more fun. Play this video while you do other things.","depth":15,"bounds":{"left":0.7083333,"top":0.76166666,"width":0.14652778,"height":0.07722222},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pop out this video","depth":17,"bounds":{"left":0.63506943,"top":0.77666664,"width":0.07569444,"height":0.017777778},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"More screens are more fun. Play this video while you do other things.","depth":16,"bounds":{"left":0.615625,"top":0.7722222,"width":0.11736111,"height":0.045},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Lukas Kovalik","depth":17,"bounds":{"left":0.753125,"top":0.84166664,"width":0.06875,"height":0.022777777},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Others might see more of your background. Click to view your full video.","depth":14,"bounds":{"left":0.96631944,"top":0.8383333,"width":0.018055556,"height":0.028888889},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"10:15","depth":12,"bounds":{"left":0.050347224,"top":0.9444444,"width":0.02673611,"height":0.022777777},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"AM","depth":12,"bounds":{"left":0.08055556,"top":0.9444444,"width":0.017708333,"height":0.022777777},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Daily - Platform","depth":12,"bounds":{"left":0.115625,"top":0.9111111,"width":0.08055556,"height":0.08888888},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Daily - Platform","depth":15,"bounds":{"left":0.115625,"top":0.9444444,"width":0.08055556,"height":0.022777777},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Audio settings","depth":14,"bounds":{"left":0.32118055,"top":0.9288889,"width":0.06111111,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn off microphone","depth":14,"bounds":{"left":0.34895834,"top":0.9288889,"width":0.033333335,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":true,"is_selected":false},{"role":"AXButton","text":"Video settings","depth":13,"bounds":{"left":0.38784721,"top":0.9288889,"width":0.06111111,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn off camera","depth":13,"bounds":{"left":0.415625,"top":0.9288889,"width":0.033333335,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Nikolay Yankov is presenting","depth":12,"bounds":{"left":0.45451388,"top":0.9288889,"width":0.03888889,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Send a reaction","depth":12,"bounds":{"left":0.49895832,"top":0.9288889,"width":0.03888889,"height":0.053333335},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn on captions","depth":13,"bounds":{"left":0.5434028,"top":0.9288889,"width":0.03888889,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Raise hand (ctrl + ⌘ + h)","depth":12,"bounds":{"left":0.58784723,"top":0.9288889,"width":0.03888889,"height":0.053333335},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options","depth":12,"bounds":{"left":0.6322917,"top":0.9288889,"width":0.025,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Leave call","depth":12,"bounds":{"left":0.6628472,"top":0.9288889,"width":0.05,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Meeting details","depth":12,"bounds":{"left":0.89166665,"top":0.9288889,"width":0.033333335,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat with everyone","depth":12,"bounds":{"left":0.925,"top":0.9288889,"width":0.033333335,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Meeting tools","depth":12,"bounds":{"left":0.9583333,"top":0.9288889,"width":0.033333335,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Nikolay Ivanov has left the meeting","depth":8,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-602531024127880156
|
-6426241406234068172
|
visual_change
|
accessibility
|
NULL
|
Meet - Daily - Platform
Close tab
New Tab
Open Goo Meet - Daily - Platform
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
4
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Zoom in
Open in new window
Enter Full Screen
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Nikolay Yankov
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Aneliya Angelova
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Lukas Kovalik
Others might see more of your background. Click to view your full video.
10:15
AM
Daily - Platform
Daily - Platform
Audio settings
Turn off microphone
Video settings
Turn off camera
Nikolay Yankov is presenting
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Nikolay Ivanov has left the meeting...
|
73296
|
|
73294
|
NULL
|
0
|
2026-04-23T07:15:29.210429+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776928529210_m2.jpg...
|
Firefox
|
Meet - Daily - Platform — Work
|
True
|
meet.google.com/agt-teir-cwt?authuser=lukas.kovali meet.google.com/agt-teir-cwt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - Daily - Platform
Close tab
New Tab
Open Goo Meet - Daily - Platform
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
7
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Unpin Nikolay Yankov's presentation from your main screen
You can't unmute someone else's presentation
More options for Nikolay Yankov
Zoom in
Open in new window
Enter Full Screen
Pin Steliyan Georgiev to your main screen
Mute Steliyan Georgiev's microphone
More options for Steliyan Georgiev
Steliyan Georgiev
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Pin Nikolay Nikolov to your main screen
Mute Nikolay Nikolov's microphone
More options for Nikolay Nikolov
Nikolay Nikolov
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Pin Nikolay Yankov to your main screen
Mute Nikolay Yankov's microphone
More options for Nikolay Yankov
Nikolay Yankov
2 others
2 others
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
Others might see more of your background. Click to view your full video.
10:15
AM
Daily - Platform
Daily - Platform
Audio settings
Turn off microphone
Video settings
Turn off camera
Nikolay Yankov is presenting
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Your microphone is on....
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - Daily - Platform","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.016123671,"height":-0.051875472},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.27094415,"top":1.0,"width":0.004986702,"height":-0.051875472},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.27310506,"top":1.0,"width":0.010638298,"height":-0.086193085},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Nikolay Yankov (Presenting)","depth":12,"bounds":{"left":0.30634972,"top":1.0,"width":0.059507977,"height":-0.072625756},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Nikolay Yankov (Presenting)","depth":13,"bounds":{"left":0.30634972,"top":1.0,"width":0.059507977,"height":-0.07342374},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"People","depth":15,"bounds":{"left":0.69498,"top":1.0,"width":0.019448139,"height":-0.06424582},"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"7","depth":22,"bounds":{"left":0.70827794,"top":1.0,"width":0.0021609042,"height":-0.072625756},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Take notes with Gemini","depth":14,"bounds":{"left":0.71708775,"top":1.0,"width":0.011968086,"height":-0.06424582},"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Take notes with Gemini","depth":17,"bounds":{"left":0.7184175,"top":1.0,"width":0.043550532,"height":-0.072625756},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Gemini","depth":22,"bounds":{"left":0.7330452,"top":1.0,"width":0.013464096,"height":-0.072625756},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Gemini","depth":21,"bounds":{"left":0.73204786,"top":1.0,"width":0.011303191,"height":-0.065043926},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Pop out this video More screens are more fun. Play this video while you do other things.","depth":15,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pop out this video","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"More screens are more fun. Play this video while you do other things.","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Unpin Nikolay Yankov's presentation from your main screen","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"You can't unmute someone else's presentation","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Nikolay Yankov","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Zoom in","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open in new window","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Enter Full Screen","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Pin Steliyan Georgiev to your main screen","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Mute Steliyan Georgiev's microphone","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Steliyan Georgiev","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Steliyan Georgiev","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pop out this video More screens are more fun. Play this video while you do other things.","depth":15,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pop out this video","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"More screens are more fun. Play this video while you do other things.","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Nikolay Nikolov to your main screen","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Mute Nikolay Nikolov's microphone","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Nikolay Nikolov","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Nikolay Nikolov","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pop out this video More screens are more fun. Play this video while you do other things.","depth":15,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pop out this video","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"More screens are more fun. Play this video while you do other things.","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Nikolay Yankov to your main screen","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Mute Nikolay Yankov's microphone","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Nikolay Yankov","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Nikolay Yankov","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"2 others","depth":11,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"2 others","depth":13,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pop out this video More screens are more fun. Play this video while you do other things.","depth":15,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pop out this video","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"More screens are more fun. Play this video while you do other things.","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"You’re continuously framed","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Backgrounds and effects","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Lukas Kovalik","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Lukas Kovalik","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Others might see more of your background. Click to view your full video.","depth":14,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"10:15","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"AM","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Daily - Platform","depth":12,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Daily - Platform","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Audio settings","depth":14,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn off microphone","depth":14,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":true,"is_selected":false},{"role":"AXButton","text":"Video settings","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn off camera","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Nikolay Yankov is presenting","depth":12,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Send a reaction","depth":12,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn on captions","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Raise hand (ctrl + ⌘ + h)","depth":12,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options","depth":12,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Leave call","depth":12,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Meeting details","depth":12,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat with everyone","depth":12,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Meeting tools","depth":12,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Your microphone is on.","depth":8,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
7871597623300975993
|
-6426107538543281380
|
visual_change
|
hybrid
|
NULL
|
Meet - Daily - Platform
Close tab
New Tab
Open Goo Meet - Daily - Platform
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
7
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Unpin Nikolay Yankov's presentation from your main screen
You can't unmute someone else's presentation
More options for Nikolay Yankov
Zoom in
Open in new window
Enter Full Screen
Pin Steliyan Georgiev to your main screen
Mute Steliyan Georgiev's microphone
More options for Steliyan Georgiev
Steliyan Georgiev
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Pin Nikolay Nikolov to your main screen
Mute Nikolay Nikolov's microphone
More options for Nikolay Nikolov
Nikolay Nikolov
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Pin Nikolay Yankov to your main screen
Mute Nikolay Yankov's microphone
More options for Nikolay Yankov
Nikolay Yankov
2 others
2 others
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
Others might see more of your background. Click to view your full video.
10:15
AM
Daily - Platform
Daily - Platform
Audio settings
Turn off microphone
Video settings
Turn off camera
Nikolay Yankov is presenting
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Your microphone is on.
HomeActivit)Jiminny ... ~# product_launches# randomi releases# sofia-office# support# thank-yous# the people of jimi..A Direct messages(3) Aneliva Angelo..Stoyan TomovNikolay Yankovf Petko Kashinski. Aneliya Angelovae. Nikolay Nikolov# Mario GeorgievSe: Todor StamatovA Gabriela Dureva•. Vasil Vasilev. Galva Dimitrovan Stefka StoyanovaStoyan TanevNikolav Ivanov#:Apps• Toast" Jira CloudINavicateCodeStoyan TomovMessagesAdd canvaur Files& PinsLukas Kovslib c.co.onами не з + 3 new messagesХ и моке би 22тора ми са поасни.но да питай Галя ако иска да сменяваdeal risk трябва да ти каже че сделка е подписк. ако са еключени ссhесued ти няма какда го отвориш и да виждаш, то трябва да еkato nlavbackStoyan Tomov 9:55 AMвсе оше пи глеламе за нови слелки рхъбспот на 30 минути или и това е сwehhooks?Lukas Kovallk 10-12AMлобно утло эми май в воце на 5 мин отStovan Tomoy 10:14 AMто това май важи и за контакти и акаунти.или се бипкам.Lukas Kovallk 10:15 AMna. sunc obiectsStovan Tomov 10:15AMIcvnen nooneMessage Stoyan TomovAaFV faVsco.js°9 JY-20157-AJ-report-not-send-notificatProiect= custom.log= laravel.log© EventServiceProvider.php© HubspotJournalServiceProvider.pAskJminnykeporacuvilyservice.org© HubspotWebhookServiceProvide© JiminnyServiceProvider.php(c) PlanhatServiceProvider.pngAutomatedreporiskepository.ono xpnp api vz.ohp© ProphetHandlerServiceProvider.p© PusherServiceProvider.php29.07.25 Nikolov20,04.26 caudelbot 208© QueueLogServiceProvider.php© QueueStatsdServiceProvider.php© ResponseMacroServiceProvider.r20.04.26 claude[bot)20.04.26 claude[bot]© RouteServiceProvider.phplusage© SsoServiceProvider.php18.08.25 Ivanov3.09.25 Ivanov© UtilServiceProvider.php© AutomatedReportsCommand.phpA SF [jiminny@localhost]-repori-not-generated.dlade.png©) PlannatActivityListener.php# console PRODclass AutomatedreportsRepository416 X721 A Y© DealsRepositorv.php xououc runction tindlatestderau.torrarledresu tAutomatedRenort srevort: Automatedc) ViewercuaroserviceProvider.ono>0 Queue3.09.25v → Repositories4.09.25 Ivanov>Ai4.09.25 Ivanov4.09.25 Ivanov> → AutoScorina› D Calendar4.09.25 Ivanovverm4.09.25 Ivanov4.09.25 Ivanov© AccountRepository.php4.09.25 Ivanov© ContactRepository.php© ContactRoleRepository.php4.09.25 Ivanov4.09.25 Ivanov© CrmConfigurationRepository.p23.10.25 vanov© CrmEntityRepository.php3.09.25© FieldDataRepository.php© FieldRepository.php3.09.25vanov© LayoutEntityRepository.php3.09.25 Ivanov© LayoutRepository.php3.09.25 vanov21.04.26 Kovalik© LeadRepository.php© OpportunityRepository.php73.10.25 Ivanov3.09.25© ProfileRepository.phpIvanov3.09.25Ivanov© RecordTypeFieldValuesReposi© StageRepository.php3.09.25 Ivanov© SyncBatchRepository.php3.09.25 Ivanov> C Geography3.09.25 Ivanov© ActiveStreamsRepositorv.php3.09.25Ivanov© ActivityCommentRepository.php3.09.25 Ivanov© ActivityLoaRepository.php4.09.25 Ivanov© ActivitvMessageRepository.php10.09.25 Ivanov@ActivitvMomentRepositorv.oh3.09.25 Ivanov@ActivitvProviderRepositorv.ohp3.09.25 Ivanov@ActivitvRepositorv.ohn4.09.25 Ivanov@ActivitvSearchFilterRepositorv.oh10.09.25 Ivanov@ ActivitvShareRepositorv.ohn3.09.25 Ivanov© ActivityUploadSettingRepository-i3.09.25 Ivanov23.10.25 Ivanov(C) A'[EMAIL] & console (EUpublic function getGeneratedNotSentResults: Collection(...}— 141=143class I wmA36 ^public function ge$qb = Activity::with(l1 usagepublic function getPaginatedUserReports(146ReportSortReportSortDirection $sortDirectionsresuutsPerPagespage?larbon?larbonstrombatestobateoteamlos.->orderyscriter*-149150_151152// Should we filter acreturn $qb->get()->all154Pstrina Snamel): LenathAwarePaginator {155 Gt > ic function aetStagesiSquerv = AutomatedRevortResult::queruo->whereNotNul column180 @ › 1c function getConfigu'automated_report_results.report_id': 197->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))›ic function getPipelir->orderßvRaw sas->select( c->with relations: "renort.team'):›ate function creatediif SfromDate le= null && Stolate le= nulb<229Cauenv->wheneRetween colunn: Iaenenated att ICfromlate Ctolateln.pplies all applicablei+(1 emntv(Steamtde))$|232233234return stringl]Squery->where(function ($q) use (SteamIds) {...}):235249,gee funation visitcoueif (! empty(SreportTypes)) {Squery->whereIn( ccaucomaced reporcs.cype, sreportlypes256,3ee funation nudratestif (4 emptv(Sname)){Sauery->wherelike column:"name'.-270param intl] $dealids23.10.25 Ivanov23.10.25 Ivanov9 AutomatedReportsRepository.php@ CallimportRepository.php23.10.25 vanov(C) CoachinaFeedhackRenasitorv nhil3.09.25 Ivanov@ CrmTemnlateFilterRenocitorv nhr 3.09.25 Ivanov› ate function fetchstagreturn Squery->paqnnateSresultsPerPage. '*")..pageName: "page', Spage):userollot Intoductl.. In 1h 10mInu Z3 Aor 10:10.34RequestGenerateAskJiminnyReportJobTest ~+0 ..CascadeAutomated Reports LOusers (explicitelly). For other reports the groups are used for filtering and we should only show results if the user is inThouaht for 24glYou're right. The app lyUserAccessScope logic is actually correct for access control, but countUserReports doesn't match it.Here's the breakdown:Current annivllserAccecsScone (correct)Ask Jiminny: Access via groups OR recipients.users• Non-Ask Jiminny: Access via recipients.users onlycountUserReports (missing aroup access): Onlv checks recipients.users - undercounts for Ask Jiminny users with aroupFix for countllserRenorts!• AutomatedReportsRepository.php:271-281nublic function countllserRenorts(llser Suser)- intlSuserId = $user->getId():SgroupId = $user->getGroupId():returnAutomatedRenortResult:quervl)→wherenas report,function ($q) use (Suser, SuserId, $groupld) {Sa->where"team_id', Suser->qetTeamido)swhere(function (sa)) use (Suserld. Carountd) {if (SgroupId !== null) {$a2->orWhere function Ssub use (Saroupid)Scuh-swhere/Itvne' AutomatedRenortcService:-TYPF_ASK 1TMTNNY->whereJsonContains('groups'. SaroupId):->counto:Regarding the teamIds filter (lines 252-258): This works correctly as a content filter. When users pass ?team []=A, they'refiltering to see reports that include group A in their filter criteria. This is separate from access controlPotential edge case to verify: Non-Ask Jiminny reports with empty groups array won't appear when filtering by specific teamsThis is orobablv intended - if a report filters by "no arouns" it shouldn't show when user asks for "arouo A renorts"The transformer change at line 139 is not okay without this fix - it will show false for users who only have Ask Jiminny accessvia arounsral .Ask anvthing (*4L)C° AdantivePo 4 spac...
|
NULL
|
|
73266
|
NULL
|
0
|
2026-04-23T07:10:41.686036+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776928241686_m2.jpg...
|
Slack
|
Aneliya Angelova, Nikolay Yankov, Steliyan Georgie Aneliya Angelova, Nikolay Yankov, Steliyan Georgiev (DM) - Jiminny Inc - 4 new items - Slack...
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later...
|
[{"role":"AXPopUpButton","text [{"role":"AXPopUpButton","text":"Switch workspaces… (Jiminny Inc) Has new messages","depth":14,"bounds":{"left":0.0056515955,"top":0.058260176,"width":0.011968086,"height":0.028731046},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"Home","depth":14,"bounds":{"left":0.0029920214,"top":0.10055866,"width":0.017287234,"height":0.054269753},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Home","depth":16,"bounds":{"left":0.0066489363,"top":0.13806863,"width":0.009973404,"height":0.0103751},"role_description":"text"},{"role":"AXRadioButton","text":"DMs","depth":14,"bounds":{"left":0.0029920214,"top":0.15482841,"width":0.017287234,"height":0.054269753},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"DMs","depth":16,"bounds":{"left":0.0076462766,"top":0.19233839,"width":0.007978723,"height":0.0103751},"role_description":"text"},{"role":"AXRadioButton","text":"Activity","depth":14,"bounds":{"left":0.0029920214,"top":0.20909816,"width":0.017287234,"height":0.054269753},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Activity","depth":16,"bounds":{"left":0.004986702,"top":0.24660814,"width":0.012965426,"height":0.0103751},"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":14,"bounds":{"left":0.0029920214,"top":0.26336792,"width":0.017287234,"height":0.054269753},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":16,"bounds":{"left":0.0076462766,"top":0.3008779,"width":0.0076462766,"height":0.0103751},"role_description":"text"},{"role":"AXRadioButton","text":"Later","depth":14,"bounds":{"left":0.0029920214,"top":0.31763768,"width":0.017287234,"height":0.054269753},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8884484407622198276
|
-8120845959807158105
|
idle
|
hybrid
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
FilesLaterMoreSlackcalJiminny...# deal-insights-dev# engineering# frontend# general# infra-changes# jiminny-bg8 people-with-copilo...8 people-with-zoom-.# platform-team# platform-tickets# product launches# random# releases# sofa-office# support# thank-yous# the_people_of jimi...• Direct messagesR?. Stoyan TomovB Aneliya Angelova, ...R.. Nikolay Yankov&. Petko KashinskiP. Aneliya Angelova. Nikolay Nikolovf. Mario Georgiev%: Todor StamatovMistonWindowhelp@ Describe what you are looking for& Aneliya Angelova, ...84• MessagesAdd canvaUr FilesTmkas Kovallk 2.47 pk,•може даTodayvAneliya Angelova 10:00 AMЛукаш ако трябва рьчно да се рерьнне заклиент - с тази команда ще се случва нали.няма друг начин. (edited)•питам заради седмичния периодідали не е по-добре периода да бьдепонеделник-неделя на предходнатаeeawwlr MelwtelesoihriratornCirlolLukas Kovalik 10-08 AMIхм даAneliya Angelova 10.08 AMза да мимикира точно както еаетоматичното генепипане на пепопта -може ли да е за фиксирана седмица назадфилтьоа и това което се изписва във плоа.Lukas Kovallk 10:09 AMи аз се чулих. то за това с получи.разминаванєPDF мисля че беше 7 лни на зад и activitvбяха от понеделник до неделяреално ако се пуска от крон иоо тряова да е ок+ Aa €Shift + Return to add a new ling•..DNew TabAl reports promotion pages by nik• Jy-9712 | Nuges to expire after on8 JiminnyNew Tal& JIMINNY@ For you• RecentI Starred8+ Apps0, Spaces+ ***RecentJiminny (New)+ **•ID Platform TeamWID SE KanbanIID Capture TeamID Enterprise Stability I...Processing Team(4 Service-Desk= More spaces=FiltersDashboards@ Operations2 Confluence: Teams95 Customise sidebar•40 © l Userpilot Introducti... in 1h 20m100% C4a &• Thu 23 Apr 10:10:43ny.aulasslan.netifa/sorware/c/oroecis/sy/ooarass/• Searchi+ CreateASK ROVO 1® Sô @Spaces / Jiminny (New)Platform Team ?+@ Summary& TimelineE Backlog|k''I Active sprints© CalendarReports4 Testing Board # List E Forms E Components % Development ⅘ Code O Security & Releases A) DeploymentsMore 5 +Q Search board000008Epic vType vLabel vQuick filters vComplete sprintGroup: QueriesREADY FOR DEV 4IN DEV 9CODE REVIEW 1BLOCKEDQADEPLOY 8Investigate and tix whvexceed Fontawesomepackage limitsMAINTENANCEReady for DevE.20564 1 86 0000=0Notify a user before the AJReport expiresAJ REPORTSBacklog.JY-20508Uoarade BE libraries - AoMAINTENANCEBackloa|/ JY-19957 1 •e•Sentry Hubspot Rate limitBacklog* JY-20725PO ACCEPTANCEIPart2. Automated reportswith Ask JiminnyAJ REPORTSIn DevAI) (BE) (FE) QA•..18909 8 @0000=*Change forever nudges to1 year expirationCOST-EFFECTIVE AND FASTER NU….Code ReviewD JY-97124.5 87 = 0Delays in CRM SyncPLATFORM STABTUTYIn DevI ..0553 3.5 72 •000=@Rework Nudges - Phase 2the indexed_at periodCOST-EFFECTIVE AND FASTER NU...In Dev[ JY-2048915 •=0AI Reports > Empty pagedesign and promotionAJ REPORTSIIn DevQJY-20372 6 82•=0Send email notificationwhen the report is notaeneratedAJ REPORTSIn Dev[ JY-20157AI Review - @1 - SummarviIAction items/Key PointsGROWTH - MAINTAIN OUR COMP…Evaluation for AI ActivityTypesAUTO-DETECTED ACTIVITY TYPEDeployed[J-19798 l •Prepare fallback with emailfor SSO for "nersistent)name_id_formatREDUCE CHURNClosed[...20632 1 í2 •000=QAJ Panorama> Don't showinternal errors tocustomersASK ANYTHING ON ANYTHINGDeployedProphet*...20278 1 0 •000=1Upgrade Python andlibraries - AprMAINTENANCEDeployedP.19967 10 •000=QCLONE - [Team insights)Filter aets resetautomaticallySUPPORT TICKETSDeployed*4 JY-20681 0.5 82 0000.Issue with reconnecting...
|
73264
|
|
73265
|
NULL
|
0
|
2026-04-23T07:10:37.022041+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776928237022_m1.jpg...
|
Slack
|
Aneliya Angelova, Nikolay Yankov, Steliyan Georgie Aneliya Angelova, Nikolay Yankov, Steliyan Georgiev (DM) - Jiminny Inc - 4 new items - Slack...
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
c-learning-people
confusion-clinic
curiosity_lab
deal-insights-dev
engineering
frontend
general
infra-changes
jiminny-bg
people-with-copilot-licences
people-with-zoom-phone-licences
platform-team
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Stoyan Tomov
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Nikolay Yankov
Petko Kashinski
Aneliya Angelova
Nikolay Nikolov
Mario Georgiev
Todor Stamatov
Gabriela Dureva
Vasil Vasilev
Galya Dimitrova
Stefka Stoyanova
Stoyan Tanev
Nikolay Ivanov
Jira Cloud
Toast
Google Calendar
Unread mentions
Messages
Messages
Add canvas
Add canvas
Files
Files
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Lukas Kovalik
Yesterday at 3:12:15 PM
3:12 PM
да, тогава ако е празно ще пратим email че няма нищо, и в понеделник за неделя
Aneliya Angelova
Yesterday at 3:12:40 PM
3:12 PM
да
Yesterday at 3:12:42 PM
3:12
оки
Lukas Kovalik
Yesterday at 3:13:19 PM
3:13 PM
може би тогава само без weekend
Aneliya Angelova
Yesterday at 3:14:51 PM
3:14 PM
Галя обмисля следния вариант - само за дейли репорта ако е събота и неделя да не прави нищо, а в понеделник да генерира за петък
Yesterday at 3:14:56 PM
3:14
това дали може да стане
Nikolay Yankov
Yesterday at 3:16:07 PM
3:16 PM
Това е добра идея
Lukas Kovalik
Yesterday at 3:17:04 PM
3:17 PM
може да
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Jump to date
Aneliya Angelova
Today at 10:00:12 AM
10:00 AM
Лукаш ако трябва ръчно да се рерънне за клиент - с тази команда ще се случва нали? Няма друг начин?
(edited)
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 10:00:44 AM
10:00
питам заради седмичния период
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 10:01:15 AM
10:01
дали не е по-добре периода да бъде понеделник-неделя на предходната седмица, когато се рънва ръчно
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 10:08:37 AM
10:08 AM
хм да
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Aneliya Angelova
Today at 10:08:44 AM
10:08 AM
за да мимикира точно както е автоматичното генериране на репорта - може ли да е за фиксирана седмица назад
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 10:08:57 AM
10:08
филтъра и това което се изписва във пдфа
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 10:09:09 AM
10:09 AM
и аз се чудих, то за това с получи разминаване
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 10:10:26 AM
10:10
PDF мисля че беше 7 дни на зад и activity бяха от понеделник до неделя
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
реално ако се пуска от крон йоб трябва да е
реално ако се пуска от крон йоб трябва да е
Shift + Return to add a new line
Shift + Return
to add a new line
Conversation marked as unread from this message
Aneliya Angelova is typing...
|
[{"role":"AXPopUpButton","text [{"role":"AXPopUpButton","text":"Switch workspaces… (Jiminny Inc) Has new messages","depth":14,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"Home","depth":14,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Home","depth":16,"role_description":"text"},{"role":"AXRadioButton","text":"DMs","depth":14,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"DMs","depth":16,"role_description":"text"},{"role":"AXRadioButton","text":"Activity","depth":14,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Activity","depth":16,"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":14,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":16,"role_description":"text"},{"role":"AXRadioButton","text":"Later","depth":14,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Later","depth":16,"role_description":"text"},{"role":"AXRadioButton","text":"More…","depth":14,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More","depth":16,"role_description":"text"},{"role":"AXStaticText","text":"Unreads","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Threads","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Huddles","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Drafts & sent","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Directories","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"jiminny-x-integration-app","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"platform-inner-team","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"ai-chapter","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"alerts","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"backend","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"c-learning-people","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"confusion-clinic","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"curiosity_lab","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"deal-insights-dev","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"engineering","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"frontend","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"general","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"infra-changes","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"jiminny-bg","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"people-with-copilot-licences","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"people-with-zoom-phone-licences","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"platform-team","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"platform-tickets","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"product_launches","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"random","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"releases","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"sofia-office","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"support","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"thank-yous","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"the_people_of_jiminny","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Stoyan Tomov","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":23,"role_description":"text"},{"role":"AXStaticText","text":",","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Yankov","depth":23,"role_description":"text"},{"role":"AXStaticText","text":",","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Steliyan Georgiev","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Yankov","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Petko Kashinski","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Nikolov","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Mario Georgiev","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Todor Stamatov","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Gabriela Dureva","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Vasil Vasilev","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Galya Dimitrova","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Stefka Stoyanova","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Stoyan Tanev","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Ivanov","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Jira Cloud","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Toast","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Google Calendar","depth":23,"role_description":"text"},{"role":"AXButton","text":"Unread mentions","depth":17,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"Messages","depth":17,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Messages","depth":19,"role_description":"text"},{"role":"AXRadioButton","text":"Add canvas","depth":18,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Add canvas","depth":20,"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":17,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":19,"role_description":"text"},{"role":"AXPopUpButton","text":"Add and Edit Channel Tabs","depth":17,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Canvas","depth":17,"role_description":"text"},{"role":"AXStaticText","text":"List","depth":17,"role_description":"text"},{"role":"AXStaticText","text":"Folder","depth":17,"role_description":"text"},{"role":"AXPopUpButton","text":"Jump to date","depth":23,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"role_description":"text"},{"role":"AXLink","text":"Yesterday at 3:12:15 PM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3:12 PM","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"да, тогава ако е празно ще пратим email че няма нищо, и в понеделник за неделя","depth":25,"role_description":"text"},{"role":"AXButton","text":"Aneliya Angelova","depth":24,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"role_description":"text"},{"role":"AXLink","text":"Yesterday at 3:12:40 PM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3:12 PM","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"да","depth":25,"role_description":"text"},{"role":"AXLink","text":"Yesterday at 3:12:42 PM","depth":25,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3:12","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"оки","depth":25,"role_description":"text"},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"role_description":"text"},{"role":"AXLink","text":"Yesterday at 3:13:19 PM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3:13 PM","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"може би тогава само без weekend","depth":25,"role_description":"text"},{"role":"AXButton","text":"Aneliya Angelova","depth":24,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"role_description":"text"},{"role":"AXLink","text":"Yesterday at 3:14:51 PM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3:14 PM","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"Галя обмисля следния вариант - само за дейли репорта ако е събота и неделя да не прави нищо, а в понеделник да генерира за петък","depth":25,"role_description":"text"},{"role":"AXLink","text":"Yesterday at 3:14:56 PM","depth":25,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3:14","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"това дали може да стане","depth":25,"role_description":"text"},{"role":"AXButton","text":"Nikolay Yankov","depth":24,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"role_description":"text"},{"role":"AXLink","text":"Yesterday at 3:16:07 PM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3:16 PM","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"Това е добра идея","depth":25,"role_description":"text"},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"role_description":"text"},{"role":"AXLink","text":"Yesterday at 3:17:04 PM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3:17 PM","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"може да","depth":25,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Jump to date","depth":23,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Aneliya Angelova","depth":24,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"role_description":"text"},{"role":"AXLink","text":"Today at 10:00:12 AM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:00 AM","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"Лукаш ако трябва ръчно да се рерънне за клиент - с тази команда ще се случва нали? Няма друг начин?","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"(edited)","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 10:00:44 AM","depth":25,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:00","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"питам заради седмичния период","depth":25,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 10:01:15 AM","depth":25,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:01","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"дали не е по-добре периода да бъде понеделник-неделя на предходната седмица, когато се рънва ръчно","depth":25,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"role_description":"text"},{"role":"AXLink","text":"Today at 10:08:37 AM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:08 AM","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"хм да","depth":25,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Aneliya Angelova","depth":24,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"role_description":"text"},{"role":"AXLink","text":"Today at 10:08:44 AM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:08 AM","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"за да мимикира точно както е автоматичното генериране на репорта - може ли да е за фиксирана седмица назад","depth":25,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 10:08:57 AM","depth":25,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:08","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"филтъра и това което се изписва във пдфа","depth":25,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"role_description":"text"},{"role":"AXLink","text":"Today at 10:09:09 AM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:09 AM","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"и аз се чудих, то за това с получи разминаване","depth":25,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 10:10:26 AM","depth":25,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:10","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"PDF мисля че беше 7 дни на зад и activity бяха от понеделник до неделя","depth":25,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"реално ако се пуска от крон йоб трябва да е","depth":23,"value":"реално ако се пуска от крон йоб трябва да е","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"реално ако се пуска от крон йоб трябва да е","depth":25,"role_description":"text"},{"role":"AXButton","text":"Shift + Return to add a new line","depth":20,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Shift + Return","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"to add a new line","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Conversation marked as unread from this message","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"Aneliya Angelova is typing","depth":11,"role_description":"text"}]...
|
7920613189154529785
|
-1424943058249168812
|
idle
|
hybrid
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
c-learning-people
confusion-clinic
curiosity_lab
deal-insights-dev
engineering
frontend
general
infra-changes
jiminny-bg
people-with-copilot-licences
people-with-zoom-phone-licences
platform-team
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Stoyan Tomov
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Nikolay Yankov
Petko Kashinski
Aneliya Angelova
Nikolay Nikolov
Mario Georgiev
Todor Stamatov
Gabriela Dureva
Vasil Vasilev
Galya Dimitrova
Stefka Stoyanova
Stoyan Tanev
Nikolay Ivanov
Jira Cloud
Toast
Google Calendar
Unread mentions
Messages
Messages
Add canvas
Add canvas
Files
Files
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Lukas Kovalik
Yesterday at 3:12:15 PM
3:12 PM
да, тогава ако е празно ще пратим email че няма нищо, и в понеделник за неделя
Aneliya Angelova
Yesterday at 3:12:40 PM
3:12 PM
да
Yesterday at 3:12:42 PM
3:12
оки
Lukas Kovalik
Yesterday at 3:13:19 PM
3:13 PM
може би тогава само без weekend
Aneliya Angelova
Yesterday at 3:14:51 PM
3:14 PM
Галя обмисля следния вариант - само за дейли репорта ако е събота и неделя да не прави нищо, а в понеделник да генерира за петък
Yesterday at 3:14:56 PM
3:14
това дали може да стане
Nikolay Yankov
Yesterday at 3:16:07 PM
3:16 PM
Това е добра идея
Lukas Kovalik
Yesterday at 3:17:04 PM
3:17 PM
може да
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Jump to date
Aneliya Angelova
Today at 10:00:12 AM
10:00 AM
Лукаш ако трябва ръчно да се рерънне за клиент - с тази команда ще се случва нали? Няма друг начин?
(edited)
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 10:00:44 AM
10:00
питам заради седмичния период
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 10:01:15 AM
10:01
дали не е по-добре периода да бъде понеделник-неделя на предходната седмица, когато се рънва ръчно
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 10:08:37 AM
10:08 AM
хм да
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Aneliya Angelova
Today at 10:08:44 AM
10:08 AM
за да мимикира точно както е автоматичното генериране на репорта - може ли да е за фиксирана седмица назад
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 10:08:57 AM
10:08
филтъра и това което се изписва във пдфа
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 10:09:09 AM
10:09 AM
и аз се чудих, то за това с получи разминаване
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 10:10:26 AM
10:10
PDF мисля че беше 7 дни на зад и activity бяха от понеделник до неделя
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
реално ако се пуска от крон йоб трябва да е
реално ако се пуска от крон йоб трябва да е
Shift + Return to add a new line
Shift + Return
to add a new line
Conversation marked as unread from this message
Aneliya Angelova is typing
FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelpUserpilot Introducti... in 1h 20 ml100% 128• Thu 23 Apr 10:10:38meet.google.com/agt-teir-cwt?authuser=lukas.kovalik%40jiminny.comNikolay Yankov (Presenting)9+M Inbox (2:Platform Team %.Q Search boardKADTYOKDEYWPlatiorml800С J-97121 xyService-XQN reportsF Project P.* Font Awe XE DatadogAod parerк hJт-oreolGrok via AzureDescriptionEdit descriptionSubtasksAdd subtaskLinked work itemsAdd linked work itemActivityCan I get more info..?Pro tios press M to dWork logStatus update….Thanks23aço.dev. X3 Pipelines X | E3 Jiminny x(a ArchitecTaskelD DevC uxD Al BookmarkslGroup: QueriesIn Dev v# Improve StoryDetailsCLONE - [Team insights)Filter gets resetautomaticallyAssignee32 Steliyan GeorgievAssign to meReporter3 Steliyan Georgiev05.000Assue witn reconnecuingZohoDevelopmentOpen with VS Code!J Create branch4 commits1 pull request2 bulidsUepoyco"etwolt21"=0[Tech Davl ImprovePop out this videovomponenPlatformLes Mils activity types notpullling inSueetoeAdd optionsLabeisStory point esumaseStefka StoyanovaSteliyan Georgiev4 othersGalya Dimitrova10:10 AM | Daily - PlatformLukas Kovalik24:57Sộ3...
|
73263
|
|
73241
|
NULL
|
0
|
2026-04-23T07:05:37.170395+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776927937170_m1.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsRepository.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
16
7
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active Ask Jiminny reports whose expiry date has passed.
*
* @return Collection<AutomatedReport>
*/
public function getExpiredActiveAskJiminnyReports(): Collection
{
return AutomatedReport::where('status', true)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereNotNull('expires_at')
->where('expires_at', '<', now()->toDateString())
->get();
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('report_id', $report->getId())
->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])
->latest()
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Restrict a query on the automated_reports table to reports the given user is allowed to see.
*
* Matches the customer-facing audience:
* - explicit user recipients (recipients.users)
* - members of any of the report's groups (Ask Jiminny reports)
*/
private function applyUserAccessScope(Builder $query, User $user): void
{
$userId = $user->getId();
$groupId = $user->getGroupId();
$query
->where('automated_reports.team_id', $user->getTeamId())
->where(function (Builder $q) use ($userId, $groupId): void {
$q->whereJsonContains('automated_reports.recipients->users', $userId);
if ($groupId !== null) {
$q->orWhere(function (Builder $sub) use ($groupId): void {
$sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereJsonContains('automated_reports.groups', $groupId);
});
}
});
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
36
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\DealInsights;
use Doctrine\DBAL\Connection;
use Generator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Jiminny\Component\DealInsights\Forecast\DealData;
use Jiminny\Component\DealInsights\Forecast\DealsFilter;
use Jiminny\Component\DealInsights\QueryBuilder\QueryBuilder;
use Jiminny\Component\DealInsights\QueryBuilder\Visitor\QueryBuilderVisitorInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Services\Crm\IntegrationApp\DTO\Utils\UrlGeneratorInterface;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class DealsRepository implements DealsRepositoryInterface
{
private Connection $connection;
private ProviderRegistry $providerRegistry;
/**
* @var QueryBuilderVisitorInterface[]
*/
private array $visitors = [];
/**
* @param QueryBuilderVisitorInterface[] $visitors
*/
public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])
{
$this->connection = $connection;
$this->providerRegistry = $crmProviderRegistry;
foreach ($visitors as $visitor) {
$this->visitors[$visitor->getIdentifier()] = $visitor;
}
}
public function getDeals(CriteriaInterface $criteria): array
{
$context = $criteria->getContext();
$team = $context->getTeam();
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$this->visit($qb, $criteria);
return $this->execute($team, $crmService, $qb);
}
public function getDeal(Team $team, int $id): array
{
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$qb->andWhere('opp.id = :id')->setParameter('id', $id);
return $this->execute($team, $crmService, $qb);
}
public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])
{
$qb = new QueryBuilder($this->connection);
$qb
->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')
->from('crm_fields', 'f')
->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')
->where('f.crm_configuration_id = :crm')
->andWhere('f.object_type = :type')
->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')
->orderBy('fd.object_id', 'ASC')
->addOrderBy('fd.updated_at', 'ASC')
->setParameter('type', Field::OBJECT_OPPORTUNITY)
->setParameter('crm', $crmId)
;
if (! empty($crmFields)) {
$fields = array_map(fn ($value): string => '"' . $value . '"', $crmFields);
$qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');
}
return $qb->executeQuery()->fetchAllAssociative();
}
public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAssociative();
}
public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('COALESCE(opp.currency_code, "' . $defaultCurrency . '") AS currency')
->addSelect('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
->groupBy('currency')
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAllAssociative();
}
public function getDealActivities(CriteriaInterface $criteria): array
{
$qb = Activity::with(['participants', 'user'])
->where('opportunity_id', $criteria->getOpportunityId())
->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())
->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())
->orderBy($criteria->getSortBy(), $criteria->getSortDirection())
;
// Should we filter activities by criteria? It's intended to filter deals.
return $qb->get()->all();
}
public function getStages(CriteriaInterface $criteria): array
{
$qb = new QueryBuilder($this->connection);
$qb
->select('id', 'label', 'sequence')
->from('stages', 's')
->where('crm_configuration_id = :crm_configuration_id')
->andWhere('type = :type')
->orderBy('sequence', 'ASC')
->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())
->setParameter('type', Stage::TYPE_OPPORTUNITY);
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$result[$row['id']] = [
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
public function getConfigurationStages(Configuration $configuration): Collection
{
return $configuration
->stages()
->where('type', Stage::TYPE_OPPORTUNITY)
->get();
}
public function getPipelineData(Configuration $crm): array
{
$qb = new QueryBuilder($this->connection);
$provider = $crm->provider;
$qb
->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')
->from('stages', 's')
->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')
->where('s.crm_configuration_id = :crm_configuration_id')
->andWhere('s.type = :type')
->orderBy('bps.business_process_id', 'ASC')
->addOrderBy('s.sequence', 'ASC')
->setParameter('crm_configuration_id', $crm->id)
->setParameter('type', Stage::TYPE_OPPORTUNITY)
;
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];
$result[$row['pipeline_id']][] = [
'value' => $value,
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
private function createQueryBuilder(string $realm): QueryBuilder
{
return (new QueryBuilder($this->connection))
->setRealm($realm)
->from('opportunities', 'opp')
->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')
->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')
->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')
;
}
/**
* Applies all applicable visitors and returns the IDs of the executed ones
*
* @return string[]
*/
private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array
{
$queryVisitors = [];
foreach ($this->visitors as $visitor) {
if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {
$visitor->visit($queryBuilder, $criteria);
$queryVisitors[] = $visitor->getIdentifier();
}
}
return $queryVisitors;
}
private function hydrateStages(array $deals): array
{
foreach ($this->fetchStages(array_keys($deals)) as $stage) {
$oppId = (int) $stage['opportunity_id'];
if (! isset($deals[$oppId])) {
continue; // or throw??!
}
$deals[$oppId]['stages'][] = [
'id' => $stage['stage_id'],
'name' => $stage['label'],
'enteredAt' => $stage['created_at'],
];
}
return $deals;
}
/**
* @param int[] $dealIds
*/
private function fetchStages(array $dealIds): array
{
if (empty($dealIds)) {
return [];
}
$qb = new QueryBuilder($this->connection);
$qb
->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')
->from('opportunity_stages', 'os')
->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')
->where($qb->expr()->in('os.opportunity_id', $dealIds))
->orderBy('os.opportunity_id', 'ASC')
->addOrderBy('s.created_at', 'ASC')
;
return $qb->executeQuery()->fetchAllAssociative();
}
private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array
{
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$data = [
'uuid' => RequiresUUID::toNormal($row['uuid']),
'name' => $row['name'],
'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),
'account' => [
'name' => $row['acc_name'],
'url' => $crmService->generateProviderUrl(
providerId: $row['acc_provider_id'],
objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'
),
],
'owner' => null,
'rawValue' => [
'amount' => (float) $row['value'],
'currency' => $row['currency_code'],
],
'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),
'openDate' => $row['remotely_created_at'] ?? null,
'closeDate' => $row['close_date'] ?? null,
'stages' => [],
'currentPipelineId' => $row['pipeline_id'],
'currentStage' => [
'id' => $row['stage_id'],
'enteredAt' => $row['stage_updated_at'],
],
'currentStageUpdatedAt' => $row['stage_updated_at'],
'isClosed' => (bool) $row['is_closed'],
'isWon' => (bool) $row['is_won'],
];
if (isset($row['owner_uuid'])) {
$data['owner'] = [
'uuid' => RequiresUUID::toNormal($row['owner_uuid']),
'name' => $row['owner_name'],
'photoUrl' => $row['owner_photo'] === null
? null
: client_cdn($row['owner_photo'], $team),
'id' => $row['owner_id'],
'job' => $row['owner_job'],
];
}
$result[(int) $row['opp_id']] = $data;
}
return $this->hydrateStages($result);
}
private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder
{
$qb = clone $queryBuilder;
$qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');
$qb
->select(...[
'opp.id as opp_id',
'opp.uuid',
'opp.name',
'opp.value',
'opp.currency_code',
'opp.close_date',
'opp.remotely_created_at',
'opp.is_closed',
'opp.is_won',
])
->addSelect(...[
'usr.uuid as owner_uuid',
'usr.name AS owner_name',
'usr.photo_path as owner_photo',
'usr.id AS owner_id',
'jt.name as owner_job',
])
->addSelect('opp.stage_id', 'opp.stage_updated_at')
->addSelect(...[
'acc.name AS acc_name',
'acc.is_internal as acc_is_internal',
'opp.stage_updated_at',
'acc.crm_provider_id AS acc_provider_id',
'opp.crm_provider_id AS opp_provider_id',
])
->addSelect('rt.business_process_id AS pipeline_id')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'));
return $qb;
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws SocialAccountTokenInvalidException
*/
private function getCrmService(Team $team): ServiceInterface
{
$crmService = $this->providerRegistry->get($team->crm->provider);
$crmService->setConfiguration($team->crm);
if ($crmService instanceof UrlGeneratorInterface) {
$crmService->setCrmUrlGenerator($team->crm);
}
return $crmService;
}
/**
*
* @return Generator<DealData>
*/
public function getForecastData(DealsFilter $filter): Generator
{
$opportunities = DB::query()
->select([
'o.value',
'o.close_date',
'o.currency_code',
'o.is_won',
'o.is_closed',
'o.probability',
'o.forecast_category',
])
->from('opportunities', 'o')
->join('users', 'users.id', '=', 'o.user_id')
->join('groups', 'groups.id', '=', 'users.group_id')
->where('users.team_id', $filter->getTeam()->getId())
->where('o.close_date', '>=', $filter->getStartDate())
->where('o.close_date', '<=', $filter->getEndDate())
->where('o.currency_code', $filter->getCurrency())
->where('o.deleted_at', '=', null)
;
$userUuidList = $filter->getUserUuidList();
if (! empty($userUuidList)) {
$userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);
$opportunities->whereIn('users.uuid', $userUuidList);
}
$groupUuidList = $filter->getGroupUuidList();
if (! empty($groupUuidList)) {
$groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);
$opportunities->whereIn('groups.uuid', $groupUuidList);
}
foreach ($opportunities->cursor() as $row) {
yield new DealData(
(float) $row->value,
$row->close_date,
! empty($row->is_won),
! empty($row->is_closed),
$row->probability ?: 0,
$row->forecast_category ?: '',
);
}
}
public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection
{
return $user->subscriptionSets()
->where(static function (Eloquent\Builder $query): void {
$query
->whereNull('expired_at')
->orWhere('expired_at', '>=', now());
})
->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {
$join
->on('subscription_set_id', '=', 'activity_subscription_sets.id');
$join
->where('followable_type', Models\Activity\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)
->whereIn('followable_id', $opportunityIds);
})
->pluck('followable_id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20157-AJ-report-not-send-notification, menu","depth":5,"help_text":"Git Branch: JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"RequestGenerateAskJiminnyReportJobTest","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"16","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"7","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active Ask Jiminny reports whose expiry date has passed.\n *\n * @return Collection<AutomatedReport>\n */\n public function getExpiredActiveAskJiminnyReports(): Collection\n {\n return AutomatedReport::where('status', true)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereNotNull('expires_at')\n ->where('expires_at', '<', now()->toDateString())\n ->get();\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('report_id', $report->getId())\n ->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])\n ->latest()\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Restrict a query on the automated_reports table to reports the given user is allowed to see.\n *\n * Matches the customer-facing audience:\n * - explicit user recipients (recipients.users)\n * - members of any of the report's groups (Ask Jiminny reports)\n */\n private function applyUserAccessScope(Builder $query, User $user): void\n {\n $userId = $user->getId();\n $groupId = $user->getGroupId();\n\n $query\n ->where('automated_reports.team_id', $user->getTeamId())\n ->where(function (Builder $q) use ($userId, $groupId): void {\n $q->whereJsonContains('automated_reports.recipients->users', $userId);\n\n if ($groupId !== null) {\n $q->orWhere(function (Builder $sub) use ($groupId): void {\n $sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereJsonContains('automated_reports.groups', $groupId);\n });\n }\n });\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active Ask Jiminny reports whose expiry date has passed.\n *\n * @return Collection<AutomatedReport>\n */\n public function getExpiredActiveAskJiminnyReports(): Collection\n {\n return AutomatedReport::where('status', true)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereNotNull('expires_at')\n ->where('expires_at', '<', now()->toDateString())\n ->get();\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('report_id', $report->getId())\n ->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])\n ->latest()\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Restrict a query on the automated_reports table to reports the given user is allowed to see.\n *\n * Matches the customer-facing audience:\n * - explicit user recipients (recipients.users)\n * - members of any of the report's groups (Ask Jiminny reports)\n */\n private function applyUserAccessScope(Builder $query, User $user): void\n {\n $userId = $user->getId();\n $groupId = $user->getGroupId();\n\n $query\n ->where('automated_reports.team_id', $user->getTeamId())\n ->where(function (Builder $q) use ($userId, $groupId): void {\n $q->whereJsonContains('automated_reports.recipients->users', $userId);\n\n if ($groupId !== null) {\n $q->orWhere(function (Builder $sub) use ($groupId): void {\n $sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereJsonContains('automated_reports.groups', $groupId);\n });\n }\n });\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"36","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\DealInsights;\n\nuse Doctrine\\DBAL\\Connection;\nuse Generator;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealData;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealsFilter;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\QueryBuilder;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\Visitor\\QueryBuilderVisitorInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Services\\Crm\\IntegrationApp\\DTO\\Utils\\UrlGeneratorInterface;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Eloquent;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass DealsRepository implements DealsRepositoryInterface\n{\n private Connection $connection;\n\n private ProviderRegistry $providerRegistry;\n\n /**\n * @var QueryBuilderVisitorInterface[]\n */\n private array $visitors = [];\n\n /**\n * @param QueryBuilderVisitorInterface[] $visitors\n */\n public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])\n {\n $this->connection = $connection;\n $this->providerRegistry = $crmProviderRegistry;\n\n foreach ($visitors as $visitor) {\n $this->visitors[$visitor->getIdentifier()] = $visitor;\n }\n }\n\n public function getDeals(CriteriaInterface $criteria): array\n {\n $context = $criteria->getContext();\n $team = $context->getTeam();\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n\n $this->visit($qb, $criteria);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getDeal(Team $team, int $id): array\n {\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n $qb->andWhere('opp.id = :id')->setParameter('id', $id);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')\n ->from('crm_fields', 'f')\n ->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')\n ->where('f.crm_configuration_id = :crm')\n ->andWhere('f.object_type = :type')\n ->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')\n ->orderBy('fd.object_id', 'ASC')\n ->addOrderBy('fd.updated_at', 'ASC')\n\n ->setParameter('type', Field::OBJECT_OPPORTUNITY)\n ->setParameter('crm', $crmId)\n ;\n\n if (! empty($crmFields)) {\n $fields = array_map(fn ($value): string => '\"' . $value . '\"', $crmFields);\n $qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');\n }\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAssociative();\n }\n\n public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('COALESCE(opp.currency_code, \"' . $defaultCurrency . '\") AS currency')\n ->addSelect('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ->groupBy('currency')\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getDealActivities(CriteriaInterface $criteria): array\n {\n $qb = Activity::with(['participants', 'user'])\n ->where('opportunity_id', $criteria->getOpportunityId())\n ->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())\n ->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())\n ->orderBy($criteria->getSortBy(), $criteria->getSortDirection())\n ;\n\n // Should we filter activities by criteria? It's intended to filter deals.\n\n return $qb->get()->all();\n }\n\n public function getStages(CriteriaInterface $criteria): array\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('id', 'label', 'sequence')\n ->from('stages', 's')\n ->where('crm_configuration_id = :crm_configuration_id')\n ->andWhere('type = :type')\n ->orderBy('sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())\n ->setParameter('type', Stage::TYPE_OPPORTUNITY);\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $result[$row['id']] = [\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n public function getConfigurationStages(Configuration $configuration): Collection\n {\n return $configuration\n ->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get();\n }\n\n public function getPipelineData(Configuration $crm): array\n {\n $qb = new QueryBuilder($this->connection);\n $provider = $crm->provider;\n\n $qb\n ->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')\n ->from('stages', 's')\n ->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')\n ->where('s.crm_configuration_id = :crm_configuration_id')\n ->andWhere('s.type = :type')\n ->orderBy('bps.business_process_id', 'ASC')\n ->addOrderBy('s.sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $crm->id)\n ->setParameter('type', Stage::TYPE_OPPORTUNITY)\n ;\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];\n $result[$row['pipeline_id']][] = [\n 'value' => $value,\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n private function createQueryBuilder(string $realm): QueryBuilder\n {\n return (new QueryBuilder($this->connection))\n ->setRealm($realm)\n ->from('opportunities', 'opp')\n ->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')\n ->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')\n ->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')\n ;\n }\n\n /**\n * Applies all applicable visitors and returns the IDs of the executed ones\n *\n * @return string[]\n */\n private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array\n {\n $queryVisitors = [];\n\n foreach ($this->visitors as $visitor) {\n if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {\n $visitor->visit($queryBuilder, $criteria);\n\n $queryVisitors[] = $visitor->getIdentifier();\n }\n }\n\n return $queryVisitors;\n }\n\n private function hydrateStages(array $deals): array\n {\n foreach ($this->fetchStages(array_keys($deals)) as $stage) {\n $oppId = (int) $stage['opportunity_id'];\n\n if (! isset($deals[$oppId])) {\n continue; // or throw??!\n }\n\n $deals[$oppId]['stages'][] = [\n 'id' => $stage['stage_id'],\n 'name' => $stage['label'],\n 'enteredAt' => $stage['created_at'],\n ];\n }\n\n return $deals;\n }\n\n /**\n * @param int[] $dealIds\n */\n private function fetchStages(array $dealIds): array\n {\n if (empty($dealIds)) {\n return [];\n }\n\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')\n ->from('opportunity_stages', 'os')\n ->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')\n ->where($qb->expr()->in('os.opportunity_id', $dealIds))\n ->orderBy('os.opportunity_id', 'ASC')\n ->addOrderBy('s.created_at', 'ASC')\n ;\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array\n {\n $result = [];\n\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $data = [\n 'uuid' => RequiresUUID::toNormal($row['uuid']),\n 'name' => $row['name'],\n 'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),\n 'account' => [\n 'name' => $row['acc_name'],\n 'url' => $crmService->generateProviderUrl(\n providerId: $row['acc_provider_id'],\n objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'\n ),\n ],\n 'owner' => null,\n 'rawValue' => [\n 'amount' => (float) $row['value'],\n 'currency' => $row['currency_code'],\n ],\n 'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),\n 'openDate' => $row['remotely_created_at'] ?? null,\n 'closeDate' => $row['close_date'] ?? null,\n 'stages' => [],\n 'currentPipelineId' => $row['pipeline_id'],\n 'currentStage' => [\n 'id' => $row['stage_id'],\n 'enteredAt' => $row['stage_updated_at'],\n ],\n 'currentStageUpdatedAt' => $row['stage_updated_at'],\n 'isClosed' => (bool) $row['is_closed'],\n 'isWon' => (bool) $row['is_won'],\n ];\n\n if (isset($row['owner_uuid'])) {\n $data['owner'] = [\n 'uuid' => RequiresUUID::toNormal($row['owner_uuid']),\n 'name' => $row['owner_name'],\n 'photoUrl' => $row['owner_photo'] === null\n ? null\n : client_cdn($row['owner_photo'], $team),\n 'id' => $row['owner_id'],\n 'job' => $row['owner_job'],\n ];\n }\n\n $result[(int) $row['opp_id']] = $data;\n }\n\n return $this->hydrateStages($result);\n }\n\n private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder\n {\n $qb = clone $queryBuilder;\n $qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');\n\n $qb\n ->select(...[\n 'opp.id as opp_id',\n 'opp.uuid',\n 'opp.name',\n 'opp.value',\n 'opp.currency_code',\n 'opp.close_date',\n 'opp.remotely_created_at',\n 'opp.is_closed',\n 'opp.is_won',\n ])\n ->addSelect(...[\n 'usr.uuid as owner_uuid',\n 'usr.name AS owner_name',\n 'usr.photo_path as owner_photo',\n 'usr.id AS owner_id',\n 'jt.name as owner_job',\n ])\n ->addSelect('opp.stage_id', 'opp.stage_updated_at')\n ->addSelect(...[\n 'acc.name AS acc_name',\n 'acc.is_internal as acc_is_internal',\n 'opp.stage_updated_at',\n 'acc.crm_provider_id AS acc_provider_id',\n 'opp.crm_provider_id AS opp_provider_id',\n ])\n ->addSelect('rt.business_process_id AS pipeline_id')\n\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'));\n\n return $qb;\n }\n\n /**\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws SocialAccountTokenInvalidException\n */\n private function getCrmService(Team $team): ServiceInterface\n {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n $crmService->setConfiguration($team->crm);\n if ($crmService instanceof UrlGeneratorInterface) {\n $crmService->setCrmUrlGenerator($team->crm);\n }\n\n return $crmService;\n }\n\n /**\n *\n * @return Generator<DealData>\n */\n public function getForecastData(DealsFilter $filter): Generator\n {\n $opportunities = DB::query()\n ->select([\n 'o.value',\n 'o.close_date',\n 'o.currency_code',\n 'o.is_won',\n 'o.is_closed',\n 'o.probability',\n 'o.forecast_category',\n ])\n ->from('opportunities', 'o')\n ->join('users', 'users.id', '=', 'o.user_id')\n ->join('groups', 'groups.id', '=', 'users.group_id')\n ->where('users.team_id', $filter->getTeam()->getId())\n ->where('o.close_date', '>=', $filter->getStartDate())\n ->where('o.close_date', '<=', $filter->getEndDate())\n ->where('o.currency_code', $filter->getCurrency())\n ->where('o.deleted_at', '=', null)\n ;\n\n $userUuidList = $filter->getUserUuidList();\n if (! empty($userUuidList)) {\n $userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);\n\n $opportunities->whereIn('users.uuid', $userUuidList);\n }\n\n $groupUuidList = $filter->getGroupUuidList();\n if (! empty($groupUuidList)) {\n $groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);\n\n $opportunities->whereIn('groups.uuid', $groupUuidList);\n }\n\n foreach ($opportunities->cursor() as $row) {\n yield new DealData(\n (float) $row->value,\n $row->close_date,\n ! empty($row->is_won),\n ! empty($row->is_closed),\n $row->probability ?: 0,\n $row->forecast_category ?: '',\n );\n }\n }\n\n public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection\n {\n return $user->subscriptionSets()\n ->where(static function (Eloquent\\Builder $query): void {\n $query\n ->whereNull('expired_at')\n ->orWhere('expired_at', '>=', now());\n })\n ->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n $join\n ->where('followable_type', Models\\Activity\\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)\n ->whereIn('followable_id', $opportunityIds);\n })\n ->pluck('followable_id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\DealInsights;\n\nuse Doctrine\\DBAL\\Connection;\nuse Generator;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealData;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealsFilter;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\QueryBuilder;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\Visitor\\QueryBuilderVisitorInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Services\\Crm\\IntegrationApp\\DTO\\Utils\\UrlGeneratorInterface;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Eloquent;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass DealsRepository implements DealsRepositoryInterface\n{\n private Connection $connection;\n\n private ProviderRegistry $providerRegistry;\n\n /**\n * @var QueryBuilderVisitorInterface[]\n */\n private array $visitors = [];\n\n /**\n * @param QueryBuilderVisitorInterface[] $visitors\n */\n public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])\n {\n $this->connection = $connection;\n $this->providerRegistry = $crmProviderRegistry;\n\n foreach ($visitors as $visitor) {\n $this->visitors[$visitor->getIdentifier()] = $visitor;\n }\n }\n\n public function getDeals(CriteriaInterface $criteria): array\n {\n $context = $criteria->getContext();\n $team = $context->getTeam();\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n\n $this->visit($qb, $criteria);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getDeal(Team $team, int $id): array\n {\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n $qb->andWhere('opp.id = :id')->setParameter('id', $id);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')\n ->from('crm_fields', 'f')\n ->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')\n ->where('f.crm_configuration_id = :crm')\n ->andWhere('f.object_type = :type')\n ->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')\n ->orderBy('fd.object_id', 'ASC')\n ->addOrderBy('fd.updated_at', 'ASC')\n\n ->setParameter('type', Field::OBJECT_OPPORTUNITY)\n ->setParameter('crm', $crmId)\n ;\n\n if (! empty($crmFields)) {\n $fields = array_map(fn ($value): string => '\"' . $value . '\"', $crmFields);\n $qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');\n }\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAssociative();\n }\n\n public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('COALESCE(opp.currency_code, \"' . $defaultCurrency . '\") AS currency')\n ->addSelect('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ->groupBy('currency')\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getDealActivities(CriteriaInterface $criteria): array\n {\n $qb = Activity::with(['participants', 'user'])\n ->where('opportunity_id', $criteria->getOpportunityId())\n ->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())\n ->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())\n ->orderBy($criteria->getSortBy(), $criteria->getSortDirection())\n ;\n\n // Should we filter activities by criteria? It's intended to filter deals.\n\n return $qb->get()->all();\n }\n\n public function getStages(CriteriaInterface $criteria): array\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('id', 'label', 'sequence')\n ->from('stages', 's')\n ->where('crm_configuration_id = :crm_configuration_id')\n ->andWhere('type = :type')\n ->orderBy('sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())\n ->setParameter('type', Stage::TYPE_OPPORTUNITY);\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $result[$row['id']] = [\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n public function getConfigurationStages(Configuration $configuration): Collection\n {\n return $configuration\n ->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get();\n }\n\n public function getPipelineData(Configuration $crm): array\n {\n $qb = new QueryBuilder($this->connection);\n $provider = $crm->provider;\n\n $qb\n ->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')\n ->from('stages', 's')\n ->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')\n ->where('s.crm_configuration_id = :crm_configuration_id')\n ->andWhere('s.type = :type')\n ->orderBy('bps.business_process_id', 'ASC')\n ->addOrderBy('s.sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $crm->id)\n ->setParameter('type', Stage::TYPE_OPPORTUNITY)\n ;\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];\n $result[$row['pipeline_id']][] = [\n 'value' => $value,\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n private function createQueryBuilder(string $realm): QueryBuilder\n {\n return (new QueryBuilder($this->connection))\n ->setRealm($realm)\n ->from('opportunities', 'opp')\n ->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')\n ->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')\n ->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')\n ;\n }\n\n /**\n * Applies all applicable visitors and returns the IDs of the executed ones\n *\n * @return string[]\n */\n private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array\n {\n $queryVisitors = [];\n\n foreach ($this->visitors as $visitor) {\n if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {\n $visitor->visit($queryBuilder, $criteria);\n\n $queryVisitors[] = $visitor->getIdentifier();\n }\n }\n\n return $queryVisitors;\n }\n\n private function hydrateStages(array $deals): array\n {\n foreach ($this->fetchStages(array_keys($deals)) as $stage) {\n $oppId = (int) $stage['opportunity_id'];\n\n if (! isset($deals[$oppId])) {\n continue; // or throw??!\n }\n\n $deals[$oppId]['stages'][] = [\n 'id' => $stage['stage_id'],\n 'name' => $stage['label'],\n 'enteredAt' => $stage['created_at'],\n ];\n }\n\n return $deals;\n }\n\n /**\n * @param int[] $dealIds\n */\n private function fetchStages(array $dealIds): array\n {\n if (empty($dealIds)) {\n return [];\n }\n\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')\n ->from('opportunity_stages', 'os')\n ->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')\n ->where($qb->expr()->in('os.opportunity_id', $dealIds))\n ->orderBy('os.opportunity_id', 'ASC')\n ->addOrderBy('s.created_at', 'ASC')\n ;\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array\n {\n $result = [];\n\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $data = [\n 'uuid' => RequiresUUID::toNormal($row['uuid']),\n 'name' => $row['name'],\n 'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),\n 'account' => [\n 'name' => $row['acc_name'],\n 'url' => $crmService->generateProviderUrl(\n providerId: $row['acc_provider_id'],\n objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'\n ),\n ],\n 'owner' => null,\n 'rawValue' => [\n 'amount' => (float) $row['value'],\n 'currency' => $row['currency_code'],\n ],\n 'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),\n 'openDate' => $row['remotely_created_at'] ?? null,\n 'closeDate' => $row['close_date'] ?? null,\n 'stages' => [],\n 'currentPipelineId' => $row['pipeline_id'],\n 'currentStage' => [\n 'id' => $row['stage_id'],\n 'enteredAt' => $row['stage_updated_at'],\n ],\n 'currentStageUpdatedAt' => $row['stage_updated_at'],\n 'isClosed' => (bool) $row['is_closed'],\n 'isWon' => (bool) $row['is_won'],\n ];\n\n if (isset($row['owner_uuid'])) {\n $data['owner'] = [\n 'uuid' => RequiresUUID::toNormal($row['owner_uuid']),\n 'name' => $row['owner_name'],\n 'photoUrl' => $row['owner_photo'] === null\n ? null\n : client_cdn($row['owner_photo'], $team),\n 'id' => $row['owner_id'],\n 'job' => $row['owner_job'],\n ];\n }\n\n $result[(int) $row['opp_id']] = $data;\n }\n\n return $this->hydrateStages($result);\n }\n\n private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder\n {\n $qb = clone $queryBuilder;\n $qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');\n\n $qb\n ->select(...[\n 'opp.id as opp_id',\n 'opp.uuid',\n 'opp.name',\n 'opp.value',\n 'opp.currency_code',\n 'opp.close_date',\n 'opp.remotely_created_at',\n 'opp.is_closed',\n 'opp.is_won',\n ])\n ->addSelect(...[\n 'usr.uuid as owner_uuid',\n 'usr.name AS owner_name',\n 'usr.photo_path as owner_photo',\n 'usr.id AS owner_id',\n 'jt.name as owner_job',\n ])\n ->addSelect('opp.stage_id', 'opp.stage_updated_at')\n ->addSelect(...[\n 'acc.name AS acc_name',\n 'acc.is_internal as acc_is_internal',\n 'opp.stage_updated_at',\n 'acc.crm_provider_id AS acc_provider_id',\n 'opp.crm_provider_id AS opp_provider_id',\n ])\n ->addSelect('rt.business_process_id AS pipeline_id')\n\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'));\n\n return $qb;\n }\n\n /**\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws SocialAccountTokenInvalidException\n */\n private function getCrmService(Team $team): ServiceInterface\n {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n $crmService->setConfiguration($team->crm);\n if ($crmService instanceof UrlGeneratorInterface) {\n $crmService->setCrmUrlGenerator($team->crm);\n }\n\n return $crmService;\n }\n\n /**\n *\n * @return Generator<DealData>\n */\n public function getForecastData(DealsFilter $filter): Generator\n {\n $opportunities = DB::query()\n ->select([\n 'o.value',\n 'o.close_date',\n 'o.currency_code',\n 'o.is_won',\n 'o.is_closed',\n 'o.probability',\n 'o.forecast_category',\n ])\n ->from('opportunities', 'o')\n ->join('users', 'users.id', '=', 'o.user_id')\n ->join('groups', 'groups.id', '=', 'users.group_id')\n ->where('users.team_id', $filter->getTeam()->getId())\n ->where('o.close_date', '>=', $filter->getStartDate())\n ->where('o.close_date', '<=', $filter->getEndDate())\n ->where('o.currency_code', $filter->getCurrency())\n ->where('o.deleted_at', '=', null)\n ;\n\n $userUuidList = $filter->getUserUuidList();\n if (! empty($userUuidList)) {\n $userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);\n\n $opportunities->whereIn('users.uuid', $userUuidList);\n }\n\n $groupUuidList = $filter->getGroupUuidList();\n if (! empty($groupUuidList)) {\n $groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);\n\n $opportunities->whereIn('groups.uuid', $groupUuidList);\n }\n\n foreach ($opportunities->cursor() as $row) {\n yield new DealData(\n (float) $row->value,\n $row->close_date,\n ! empty($row->is_won),\n ! empty($row->is_closed),\n $row->probability ?: 0,\n $row->forecast_category ?: '',\n );\n }\n }\n\n public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection\n {\n return $user->subscriptionSets()\n ->where(static function (Eloquent\\Builder $query): void {\n $query\n ->whereNull('expired_at')\n ->orWhere('expired_at', '>=', now());\n })\n ->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n $join\n ->where('followable_type', Models\\Activity\\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)\n ->whereIn('followable_id', $opportunityIds);\n })\n ->pluck('followable_id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3407362332434637772
|
-4167466460282672524
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
16
7
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active Ask Jiminny reports whose expiry date has passed.
*
* @return Collection<AutomatedReport>
*/
public function getExpiredActiveAskJiminnyReports(): Collection
{
return AutomatedReport::where('status', true)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereNotNull('expires_at')
->where('expires_at', '<', now()->toDateString())
->get();
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('report_id', $report->getId())
->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])
->latest()
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Restrict a query on the automated_reports table to reports the given user is allowed to see.
*
* Matches the customer-facing audience:
* - explicit user recipients (recipients.users)
* - members of any of the report's groups (Ask Jiminny reports)
*/
private function applyUserAccessScope(Builder $query, User $user): void
{
$userId = $user->getId();
$groupId = $user->getGroupId();
$query
->where('automated_reports.team_id', $user->getTeamId())
->where(function (Builder $q) use ($userId, $groupId): void {
$q->whereJsonContains('automated_reports.recipients->users', $userId);
if ($groupId !== null) {
$q->orWhere(function (Builder $sub) use ($groupId): void {
$sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereJsonContains('automated_reports.groups', $groupId);
});
}
});
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
36
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\DealInsights;
use Doctrine\DBAL\Connection;
use Generator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Jiminny\Component\DealInsights\Forecast\DealData;
use Jiminny\Component\DealInsights\Forecast\DealsFilter;
use Jiminny\Component\DealInsights\QueryBuilder\QueryBuilder;
use Jiminny\Component\DealInsights\QueryBuilder\Visitor\QueryBuilderVisitorInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Services\Crm\IntegrationApp\DTO\Utils\UrlGeneratorInterface;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class DealsRepository implements DealsRepositoryInterface
{
private Connection $connection;
private ProviderRegistry $providerRegistry;
/**
* @var QueryBuilderVisitorInterface[]
*/
private array $visitors = [];
/**
* @param QueryBuilderVisitorInterface[] $visitors
*/
public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])
{
$this->connection = $connection;
$this->providerRegistry = $crmProviderRegistry;
foreach ($visitors as $visitor) {
$this->visitors[$visitor->getIdentifier()] = $visitor;
}
}
public function getDeals(CriteriaInterface $criteria): array
{
$context = $criteria->getContext();
$team = $context->getTeam();
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$this->visit($qb, $criteria);
return $this->execute($team, $crmService, $qb);
}
public function getDeal(Team $team, int $id): array
{
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$qb->andWhere('opp.id = :id')->setParameter('id', $id);
return $this->execute($team, $crmService, $qb);
}
public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])
{
$qb = new QueryBuilder($this->connection);
$qb
->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')
->from('crm_fields', 'f')
->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')
->where('f.crm_configuration_id = :crm')
->andWhere('f.object_type = :type')
->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')
->orderBy('fd.object_id', 'ASC')
->addOrderBy('fd.updated_at', 'ASC')
->setParameter('type', Field::OBJECT_OPPORTUNITY)
->setParameter('crm', $crmId)
;
if (! empty($crmFields)) {
$fields = array_map(fn ($value): string => '"' . $value . '"', $crmFields);
$qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');
}
return $qb->executeQuery()->fetchAllAssociative();
}
public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAssociative();
}
public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('COALESCE(opp.currency_code, "' . $defaultCurrency . '") AS currency')
->addSelect('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
->groupBy('currency')
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAllAssociative();
}
public function getDealActivities(CriteriaInterface $criteria): array
{
$qb = Activity::with(['participants', 'user'])
->where('opportunity_id', $criteria->getOpportunityId())
->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())
->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())
->orderBy($criteria->getSortBy(), $criteria->getSortDirection())
;
// Should we filter activities by criteria? It's intended to filter deals.
return $qb->get()->all();
}
public function getStages(CriteriaInterface $criteria): array
{
$qb = new QueryBuilder($this->connection);
$qb
->select('id', 'label', 'sequence')
->from('stages', 's')
->where('crm_configuration_id = :crm_configuration_id')
->andWhere('type = :type')
->orderBy('sequence', 'ASC')
->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())
->setParameter('type', Stage::TYPE_OPPORTUNITY);
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$result[$row['id']] = [
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
public function getConfigurationStages(Configuration $configuration): Collection
{
return $configuration
->stages()
->where('type', Stage::TYPE_OPPORTUNITY)
->get();
}
public function getPipelineData(Configuration $crm): array
{
$qb = new QueryBuilder($this->connection);
$provider = $crm->provider;
$qb
->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')
->from('stages', 's')
->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')
->where('s.crm_configuration_id = :crm_configuration_id')
->andWhere('s.type = :type')
->orderBy('bps.business_process_id', 'ASC')
->addOrderBy('s.sequence', 'ASC')
->setParameter('crm_configuration_id', $crm->id)
->setParameter('type', Stage::TYPE_OPPORTUNITY)
;
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];
$result[$row['pipeline_id']][] = [
'value' => $value,
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
private function createQueryBuilder(string $realm): QueryBuilder
{
return (new QueryBuilder($this->connection))
->setRealm($realm)
->from('opportunities', 'opp')
->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')
->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')
->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')
;
}
/**
* Applies all applicable visitors and returns the IDs of the executed ones
*
* @return string[]
*/
private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array
{
$queryVisitors = [];
foreach ($this->visitors as $visitor) {
if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {
$visitor->visit($queryBuilder, $criteria);
$queryVisitors[] = $visitor->getIdentifier();
}
}
return $queryVisitors;
}
private function hydrateStages(array $deals): array
{
foreach ($this->fetchStages(array_keys($deals)) as $stage) {
$oppId = (int) $stage['opportunity_id'];
if (! isset($deals[$oppId])) {
continue; // or throw??!
}
$deals[$oppId]['stages'][] = [
'id' => $stage['stage_id'],
'name' => $stage['label'],
'enteredAt' => $stage['created_at'],
];
}
return $deals;
}
/**
* @param int[] $dealIds
*/
private function fetchStages(array $dealIds): array
{
if (empty($dealIds)) {
return [];
}
$qb = new QueryBuilder($this->connection);
$qb
->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')
->from('opportunity_stages', 'os')
->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')
->where($qb->expr()->in('os.opportunity_id', $dealIds))
->orderBy('os.opportunity_id', 'ASC')
->addOrderBy('s.created_at', 'ASC')
;
return $qb->executeQuery()->fetchAllAssociative();
}
private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array
{
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$data = [
'uuid' => RequiresUUID::toNormal($row['uuid']),
'name' => $row['name'],
'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),
'account' => [
'name' => $row['acc_name'],
'url' => $crmService->generateProviderUrl(
providerId: $row['acc_provider_id'],
objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'
),
],
'owner' => null,
'rawValue' => [
'amount' => (float) $row['value'],
'currency' => $row['currency_code'],
],
'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),
'openDate' => $row['remotely_created_at'] ?? null,
'closeDate' => $row['close_date'] ?? null,
'stages' => [],
'currentPipelineId' => $row['pipeline_id'],
'currentStage' => [
'id' => $row['stage_id'],
'enteredAt' => $row['stage_updated_at'],
],
'currentStageUpdatedAt' => $row['stage_updated_at'],
'isClosed' => (bool) $row['is_closed'],
'isWon' => (bool) $row['is_won'],
];
if (isset($row['owner_uuid'])) {
$data['owner'] = [
'uuid' => RequiresUUID::toNormal($row['owner_uuid']),
'name' => $row['owner_name'],
'photoUrl' => $row['owner_photo'] === null
? null
: client_cdn($row['owner_photo'], $team),
'id' => $row['owner_id'],
'job' => $row['owner_job'],
];
}
$result[(int) $row['opp_id']] = $data;
}
return $this->hydrateStages($result);
}
private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder
{
$qb = clone $queryBuilder;
$qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');
$qb
->select(...[
'opp.id as opp_id',
'opp.uuid',
'opp.name',
'opp.value',
'opp.currency_code',
'opp.close_date',
'opp.remotely_created_at',
'opp.is_closed',
'opp.is_won',
])
->addSelect(...[
'usr.uuid as owner_uuid',
'usr.name AS owner_name',
'usr.photo_path as owner_photo',
'usr.id AS owner_id',
'jt.name as owner_job',
])
->addSelect('opp.stage_id', 'opp.stage_updated_at')
->addSelect(...[
'acc.name AS acc_name',
'acc.is_internal as acc_is_internal',
'opp.stage_updated_at',
'acc.crm_provider_id AS acc_provider_id',
'opp.crm_provider_id AS opp_provider_id',
])
->addSelect('rt.business_process_id AS pipeline_id')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'));
return $qb;
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws SocialAccountTokenInvalidException
*/
private function getCrmService(Team $team): ServiceInterface
{
$crmService = $this->providerRegistry->get($team->crm->provider);
$crmService->setConfiguration($team->crm);
if ($crmService instanceof UrlGeneratorInterface) {
$crmService->setCrmUrlGenerator($team->crm);
}
return $crmService;
}
/**
*
* @return Generator<DealData>
*/
public function getForecastData(DealsFilter $filter): Generator
{
$opportunities = DB::query()
->select([
'o.value',
'o.close_date',
'o.currency_code',
'o.is_won',
'o.is_closed',
'o.probability',
'o.forecast_category',
])
->from('opportunities', 'o')
->join('users', 'users.id', '=', 'o.user_id')
->join('groups', 'groups.id', '=', 'users.group_id')
->where('users.team_id', $filter->getTeam()->getId())
->where('o.close_date', '>=', $filter->getStartDate())
->where('o.close_date', '<=', $filter->getEndDate())
->where('o.currency_code', $filter->getCurrency())
->where('o.deleted_at', '=', null)
;
$userUuidList = $filter->getUserUuidList();
if (! empty($userUuidList)) {
$userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);
$opportunities->whereIn('users.uuid', $userUuidList);
}
$groupUuidList = $filter->getGroupUuidList();
if (! empty($groupUuidList)) {
$groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);
$opportunities->whereIn('groups.uuid', $groupUuidList);
}
foreach ($opportunities->cursor() as $row) {
yield new DealData(
(float) $row->value,
$row->close_date,
! empty($row->is_won),
! empty($row->is_closed),
$row->probability ?: 0,
$row->forecast_category ?: '',
);
}
}
public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection
{
return $user->subscriptionSets()
->where(static function (Eloquent\Builder $query): void {
$query
->whereNull('expired_at')
->orWhere('expired_at', '>=', now());
})
->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {
$join
->on('subscription_set_id', '=', 'activity_subscription_sets.id');
$join
->where('followable_type', Models\Activity\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)
->whereIn('followable_id', $opportunityIds);
})
->pluck('followable_id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
|
73240
|
NULL
|
0
|
2026-04-23T07:05:19.574875+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776927919574_m2.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsRepository.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
16
7
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active Ask Jiminny reports whose expiry date has passed.
*
* @return Collection<AutomatedReport>
*/
public function getExpiredActiveAskJiminnyReports(): Collection
{
return AutomatedReport::where('status', true)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereNotNull('expires_at')
->where('expires_at', '<', now()->toDateString())
->get();
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('report_id', $report->getId())
->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])
->latest()
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Restrict a query on the automated_reports table to reports the given user is allowed to see.
*
* Matches the customer-facing audience:
* - explicit user recipients (recipients.users)
* - members of any of the report's groups (Ask Jiminny reports)
*/
private function applyUserAccessScope(Builder $query, User $user): void
{
$userId = $user->getId();
$groupId = $user->getGroupId();
$query
->where('automated_reports.team_id', $user->getTeamId())
->where(function (Builder $q) use ($userId, $groupId): void {
$q->whereJsonContains('automated_reports.recipients->users', $userId);
if ($groupId !== null) {
$q->orWhere(function (Builder $sub) use ($groupId): void {
$sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereJsonContains('automated_reports.groups', $groupId);
});
}
});
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
36
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\DealInsights;
use Doctrine\DBAL\Connection;
use Generator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Jiminny\Component\DealInsights\Forecast\DealData;
use Jiminny\Component\DealInsights\Forecast\DealsFilter;
use Jiminny\Component\DealInsights\QueryBuilder\QueryBuilder;
use Jiminny\Component\DealInsights\QueryBuilder\Visitor\QueryBuilderVisitorInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Services\Crm\IntegrationApp\DTO\Utils\UrlGeneratorInterface;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class DealsRepository implements DealsRepositoryInterface
{
private Connection $connection;
private ProviderRegistry $providerRegistry;
/**
* @var QueryBuilderVisitorInterface[]
*/
private array $visitors = [];
/**
* @param QueryBuilderVisitorInterface[] $visitors
*/
public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])
{
$this->connection = $connection;
$this->providerRegistry = $crmProviderRegistry;
foreach ($visitors as $visitor) {
$this->visitors[$visitor->getIdentifier()] = $visitor;
}
}
public function getDeals(CriteriaInterface $criteria): array
{
$context = $criteria->getContext();
$team = $context->getTeam();
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$this->visit($qb, $criteria);
return $this->execute($team, $crmService, $qb);
}
public function getDeal(Team $team, int $id): array
{
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$qb->andWhere('opp.id = :id')->setParameter('id', $id);
return $this->execute($team, $crmService, $qb);
}
public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])
{
$qb = new QueryBuilder($this->connection);
$qb
->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')
->from('crm_fields', 'f')
->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')
->where('f.crm_configuration_id = :crm')
->andWhere('f.object_type = :type')
->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')
->orderBy('fd.object_id', 'ASC')
->addOrderBy('fd.updated_at', 'ASC')
->setParameter('type', Field::OBJECT_OPPORTUNITY)
->setParameter('crm', $crmId)
;
if (! empty($crmFields)) {
$fields = array_map(fn ($value): string => '"' . $value . '"', $crmFields);
$qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');
}
return $qb->executeQuery()->fetchAllAssociative();
}
public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAssociative();
}
public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('COALESCE(opp.currency_code, "' . $defaultCurrency . '") AS currency')
->addSelect('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
->groupBy('currency')
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAllAssociative();
}
public function getDealActivities(CriteriaInterface $criteria): array
{
$qb = Activity::with(['participants', 'user'])
->where('opportunity_id', $criteria->getOpportunityId())
->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())
->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())
->orderBy($criteria->getSortBy(), $criteria->getSortDirection())
;
// Should we filter activities by criteria? It's intended to filter deals.
return $qb->get()->all();
}
public function getStages(CriteriaInterface $criteria): array
{
$qb = new QueryBuilder($this->connection);
$qb
->select('id', 'label', 'sequence')
->from('stages', 's')
->where('crm_configuration_id = :crm_configuration_id')
->andWhere('type = :type')
->orderBy('sequence', 'ASC')
->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())
->setParameter('type', Stage::TYPE_OPPORTUNITY);
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$result[$row['id']] = [
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
public function getConfigurationStages(Configuration $configuration): Collection
{
return $configuration
->stages()
->where('type', Stage::TYPE_OPPORTUNITY)
->get();
}
public function getPipelineData(Configuration $crm): array
{
$qb = new QueryBuilder($this->connection);
$provider = $crm->provider;
$qb
->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')
->from('stages', 's')
->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')
->where('s.crm_configuration_id = :crm_configuration_id')
->andWhere('s.type = :type')
->orderBy('bps.business_process_id', 'ASC')
->addOrderBy('s.sequence', 'ASC')
->setParameter('crm_configuration_id', $crm->id)
->setParameter('type', Stage::TYPE_OPPORTUNITY)
;
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];
$result[$row['pipeline_id']][] = [
'value' => $value,
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
private function createQueryBuilder(string $realm): QueryBuilder
{
return (new QueryBuilder($this->connection))
->setRealm($realm)
->from('opportunities', 'opp')
->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')
->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')
->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')
;
}
/**
* Applies all applicable visitors and returns the IDs of the executed ones
*
* @return string[]
*/
private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array
{
$queryVisitors = [];
foreach ($this->visitors as $visitor) {
if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {
$visitor->visit($queryBuilder, $criteria);
$queryVisitors[] = $visitor->getIdentifier();
}
}
return $queryVisitors;
}
private function hydrateStages(array $deals): array
{
foreach ($this->fetchStages(array_keys($deals)) as $stage) {
$oppId = (int) $stage['opportunity_id'];
if (! isset($deals[$oppId])) {
continue; // or throw??!
}
$deals[$oppId]['stages'][] = [
'id' => $stage['stage_id'],
'name' => $stage['label'],
'enteredAt' => $stage['created_at'],
];
}
return $deals;
}
/**
* @param int[] $dealIds
*/
private function fetchStages(array $dealIds): array
{
if (empty($dealIds)) {
return [];
}
$qb = new QueryBuilder($this->connection);
$qb
->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')
->from('opportunity_stages', 'os')
->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')
->where($qb->expr()->in('os.opportunity_id', $dealIds))
->orderBy('os.opportunity_id', 'ASC')
->addOrderBy('s.created_at', 'ASC')
;
return $qb->executeQuery()->fetchAllAssociative();
}
private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array
{
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$data = [
'uuid' => RequiresUUID::toNormal($row['uuid']),
'name' => $row['name'],
'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),
'account' => [
'name' => $row['acc_name'],
'url' => $crmService->generateProviderUrl(
providerId: $row['acc_provider_id'],
objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'
),
],
'owner' => null,
'rawValue' => [
'amount' => (float) $row['value'],
'currency' => $row['currency_code'],
],
'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),
'openDate' => $row['remotely_created_at'] ?? null,
'closeDate' => $row['close_date'] ?? null,
'stages' => [],
'currentPipelineId' => $row['pipeline_id'],
'currentStage' => [
'id' => $row['stage_id'],
'enteredAt' => $row['stage_updated_at'],
],
'currentStageUpdatedAt' => $row['stage_updated_at'],
'isClosed' => (bool) $row['is_closed'],
'isWon' => (bool) $row['is_won'],
];
if (isset($row['owner_uuid'])) {
$data['owner'] = [
'uuid' => RequiresUUID::toNormal($row['owner_uuid']),
'name' => $row['owner_name'],
'photoUrl' => $row['owner_photo'] === null
? null
: client_cdn($row['owner_photo'], $team),
'id' => $row['owner_id'],
'job' => $row['owner_job'],
];
}
$result[(int) $row['opp_id']] = $data;
}
return $this->hydrateStages($result);
}
private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder
{
$qb = clone $queryBuilder;
$qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');
$qb
->select(...[
'opp.id as opp_id',
'opp.uuid',
'opp.name',
'opp.value',
'opp.currency_code',
'opp.close_date',
'opp.remotely_created_at',
'opp.is_closed',
'opp.is_won',
])
->addSelect(...[
'usr.uuid as owner_uuid',
'usr.name AS owner_name',
'usr.photo_path as owner_photo',
'usr.id AS owner_id',
'jt.name as owner_job',
])
->addSelect('opp.stage_id', 'opp.stage_updated_at')
->addSelect(...[
'acc.name AS acc_name',
'acc.is_internal as acc_is_internal',
'opp.stage_updated_at',
'acc.crm_provider_id AS acc_provider_id',
'opp.crm_provider_id AS opp_provider_id',
])
->addSelect('rt.business_process_id AS pipeline_id')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'));
return $qb;
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws SocialAccountTokenInvalidException
*/
private function getCrmService(Team $team): ServiceInterface
{
$crmService = $this->providerRegistry->get($team->crm->provider);
$crmService->setConfiguration($team->crm);
if ($crmService instanceof UrlGeneratorInterface) {
$crmService->setCrmUrlGenerator($team->crm);
}
return $crmService;
}
/**
*
* @return Generator<DealData>
*/
public function getForecastData(DealsFilter $filter): Generator
{
$opportunities = DB::query()
->select([
'o.value',
'o.close_date',
'o.currency_code',
'o.is_won',
'o.is_closed',
'o.probability',
'o.forecast_category',
])
->from('opportunities', 'o')
->join('users', 'users.id', '=', 'o.user_id')
->join('groups', 'groups.id', '=', 'users.group_id')
->where('users.team_id', $filter->getTeam()->getId())
->where('o.close_date', '>=', $filter->getStartDate())
->where('o.close_date', '<=', $filter->getEndDate())
->where('o.currency_code', $filter->getCurrency())
->where('o.deleted_at', '=', null)
;
$userUuidList = $filter->getUserUuidList();
if (! empty($userUuidList)) {
$userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);
$opportunities->whereIn('users.uuid', $userUuidList);
}
$groupUuidList = $filter->getGroupUuidList();
if (! empty($groupUuidList)) {
$groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);
$opportunities->whereIn('groups.uuid', $groupUuidList);
}
foreach ($opportunities->cursor() as $row) {
yield new DealData(
(float) $row->value,
$row->close_date,
! empty($row->is_won),
! empty($row->is_closed),
$row->probability ?: 0,
$row->forecast_category ?: '',
);
}
}
public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection
{
return $user->subscriptionSets()
->where(static function (Eloquent\Builder $query): void {
$query
->whereNull('expired_at')
->orWhere('expired_at', '>=', now());
})
->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {
$join
->on('subscription_set_id', '=', 'activity_subscription_sets.id');
$join
->where('followable_type', Models\Activity\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)
->whereIn('followable_id', $opportunityIds);
})
->pluck('followable_id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.25731382,"top":0.019952115,"width":0.03856383,"height":0.025538707},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20157-AJ-report-not-send-notification, menu","depth":5,"bounds":{"left":0.29587767,"top":0.019952115,"width":0.10139628,"height":0.025538707},"help_text":"Git Branch: JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.7972075,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"RequestGenerateAskJiminnyReportJobTest","depth":6,"bounds":{"left":0.8125,"top":0.019952115,"width":0.10305851,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"16","depth":4,"bounds":{"left":0.6030585,"top":0.17478053,"width":0.009640957,"height":0.015163607},"role_description":"text"},{"role":"AXStaticText","text":"7","depth":4,"bounds":{"left":0.6146942,"top":0.17478053,"width":0.0076462766,"height":0.015163607},"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.6243351,"top":0.17478053,"width":0.00731383,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.63331115,"top":0.17318435,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.640625,"top":0.17318435,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active Ask Jiminny reports whose expiry date has passed.\n *\n * @return Collection<AutomatedReport>\n */\n public function getExpiredActiveAskJiminnyReports(): Collection\n {\n return AutomatedReport::where('status', true)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereNotNull('expires_at')\n ->where('expires_at', '<', now()->toDateString())\n ->get();\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('report_id', $report->getId())\n ->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])\n ->latest()\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Restrict a query on the automated_reports table to reports the given user is allowed to see.\n *\n * Matches the customer-facing audience:\n * - explicit user recipients (recipients.users)\n * - members of any of the report's groups (Ask Jiminny reports)\n */\n private function applyUserAccessScope(Builder $query, User $user): void\n {\n $userId = $user->getId();\n $groupId = $user->getGroupId();\n\n $query\n ->where('automated_reports.team_id', $user->getTeamId())\n ->where(function (Builder $q) use ($userId, $groupId): void {\n $q->whereJsonContains('automated_reports.recipients->users', $userId);\n\n if ($groupId !== null) {\n $q->orWhere(function (Builder $sub) use ($groupId): void {\n $sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereJsonContains('automated_reports.groups', $groupId);\n });\n }\n });\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active Ask Jiminny reports whose expiry date has passed.\n *\n * @return Collection<AutomatedReport>\n */\n public function getExpiredActiveAskJiminnyReports(): Collection\n {\n return AutomatedReport::where('status', true)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereNotNull('expires_at')\n ->where('expires_at', '<', now()->toDateString())\n ->get();\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('report_id', $report->getId())\n ->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])\n ->latest()\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Restrict a query on the automated_reports table to reports the given user is allowed to see.\n *\n * Matches the customer-facing audience:\n * - explicit user recipients (recipients.users)\n * - members of any of the report's groups (Ask Jiminny reports)\n */\n private function applyUserAccessScope(Builder $query, User $user): void\n {\n $userId = $user->getId();\n $groupId = $user->getGroupId();\n\n $query\n ->where('automated_reports.team_id', $user->getTeamId())\n ->where(function (Builder $q) use ($userId, $groupId): void {\n $q->whereJsonContains('automated_reports.recipients->users', $userId);\n\n if ($groupId !== null) {\n $q->orWhere(function (Builder $sub) use ($groupId): void {\n $sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereJsonContains('automated_reports.groups', $groupId);\n });\n }\n });\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"36","depth":4,"bounds":{"left":0.69980055,"top":0.2490024,"width":0.010305851,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7117686,"top":0.24740623,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7190825,"top":0.24740623,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\DealInsights;\n\nuse Doctrine\\DBAL\\Connection;\nuse Generator;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealData;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealsFilter;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\QueryBuilder;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\Visitor\\QueryBuilderVisitorInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Services\\Crm\\IntegrationApp\\DTO\\Utils\\UrlGeneratorInterface;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Eloquent;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass DealsRepository implements DealsRepositoryInterface\n{\n private Connection $connection;\n\n private ProviderRegistry $providerRegistry;\n\n /**\n * @var QueryBuilderVisitorInterface[]\n */\n private array $visitors = [];\n\n /**\n * @param QueryBuilderVisitorInterface[] $visitors\n */\n public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])\n {\n $this->connection = $connection;\n $this->providerRegistry = $crmProviderRegistry;\n\n foreach ($visitors as $visitor) {\n $this->visitors[$visitor->getIdentifier()] = $visitor;\n }\n }\n\n public function getDeals(CriteriaInterface $criteria): array\n {\n $context = $criteria->getContext();\n $team = $context->getTeam();\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n\n $this->visit($qb, $criteria);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getDeal(Team $team, int $id): array\n {\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n $qb->andWhere('opp.id = :id')->setParameter('id', $id);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')\n ->from('crm_fields', 'f')\n ->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')\n ->where('f.crm_configuration_id = :crm')\n ->andWhere('f.object_type = :type')\n ->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')\n ->orderBy('fd.object_id', 'ASC')\n ->addOrderBy('fd.updated_at', 'ASC')\n\n ->setParameter('type', Field::OBJECT_OPPORTUNITY)\n ->setParameter('crm', $crmId)\n ;\n\n if (! empty($crmFields)) {\n $fields = array_map(fn ($value): string => '\"' . $value . '\"', $crmFields);\n $qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');\n }\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAssociative();\n }\n\n public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('COALESCE(opp.currency_code, \"' . $defaultCurrency . '\") AS currency')\n ->addSelect('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ->groupBy('currency')\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getDealActivities(CriteriaInterface $criteria): array\n {\n $qb = Activity::with(['participants', 'user'])\n ->where('opportunity_id', $criteria->getOpportunityId())\n ->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())\n ->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())\n ->orderBy($criteria->getSortBy(), $criteria->getSortDirection())\n ;\n\n // Should we filter activities by criteria? It's intended to filter deals.\n\n return $qb->get()->all();\n }\n\n public function getStages(CriteriaInterface $criteria): array\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('id', 'label', 'sequence')\n ->from('stages', 's')\n ->where('crm_configuration_id = :crm_configuration_id')\n ->andWhere('type = :type')\n ->orderBy('sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())\n ->setParameter('type', Stage::TYPE_OPPORTUNITY);\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $result[$row['id']] = [\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n public function getConfigurationStages(Configuration $configuration): Collection\n {\n return $configuration\n ->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get();\n }\n\n public function getPipelineData(Configuration $crm): array\n {\n $qb = new QueryBuilder($this->connection);\n $provider = $crm->provider;\n\n $qb\n ->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')\n ->from('stages', 's')\n ->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')\n ->where('s.crm_configuration_id = :crm_configuration_id')\n ->andWhere('s.type = :type')\n ->orderBy('bps.business_process_id', 'ASC')\n ->addOrderBy('s.sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $crm->id)\n ->setParameter('type', Stage::TYPE_OPPORTUNITY)\n ;\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];\n $result[$row['pipeline_id']][] = [\n 'value' => $value,\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n private function createQueryBuilder(string $realm): QueryBuilder\n {\n return (new QueryBuilder($this->connection))\n ->setRealm($realm)\n ->from('opportunities', 'opp')\n ->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')\n ->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')\n ->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')\n ;\n }\n\n /**\n * Applies all applicable visitors and returns the IDs of the executed ones\n *\n * @return string[]\n */\n private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array\n {\n $queryVisitors = [];\n\n foreach ($this->visitors as $visitor) {\n if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {\n $visitor->visit($queryBuilder, $criteria);\n\n $queryVisitors[] = $visitor->getIdentifier();\n }\n }\n\n return $queryVisitors;\n }\n\n private function hydrateStages(array $deals): array\n {\n foreach ($this->fetchStages(array_keys($deals)) as $stage) {\n $oppId = (int) $stage['opportunity_id'];\n\n if (! isset($deals[$oppId])) {\n continue; // or throw??!\n }\n\n $deals[$oppId]['stages'][] = [\n 'id' => $stage['stage_id'],\n 'name' => $stage['label'],\n 'enteredAt' => $stage['created_at'],\n ];\n }\n\n return $deals;\n }\n\n /**\n * @param int[] $dealIds\n */\n private function fetchStages(array $dealIds): array\n {\n if (empty($dealIds)) {\n return [];\n }\n\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')\n ->from('opportunity_stages', 'os')\n ->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')\n ->where($qb->expr()->in('os.opportunity_id', $dealIds))\n ->orderBy('os.opportunity_id', 'ASC')\n ->addOrderBy('s.created_at', 'ASC')\n ;\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array\n {\n $result = [];\n\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $data = [\n 'uuid' => RequiresUUID::toNormal($row['uuid']),\n 'name' => $row['name'],\n 'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),\n 'account' => [\n 'name' => $row['acc_name'],\n 'url' => $crmService->generateProviderUrl(\n providerId: $row['acc_provider_id'],\n objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'\n ),\n ],\n 'owner' => null,\n 'rawValue' => [\n 'amount' => (float) $row['value'],\n 'currency' => $row['currency_code'],\n ],\n 'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),\n 'openDate' => $row['remotely_created_at'] ?? null,\n 'closeDate' => $row['close_date'] ?? null,\n 'stages' => [],\n 'currentPipelineId' => $row['pipeline_id'],\n 'currentStage' => [\n 'id' => $row['stage_id'],\n 'enteredAt' => $row['stage_updated_at'],\n ],\n 'currentStageUpdatedAt' => $row['stage_updated_at'],\n 'isClosed' => (bool) $row['is_closed'],\n 'isWon' => (bool) $row['is_won'],\n ];\n\n if (isset($row['owner_uuid'])) {\n $data['owner'] = [\n 'uuid' => RequiresUUID::toNormal($row['owner_uuid']),\n 'name' => $row['owner_name'],\n 'photoUrl' => $row['owner_photo'] === null\n ? null\n : client_cdn($row['owner_photo'], $team),\n 'id' => $row['owner_id'],\n 'job' => $row['owner_job'],\n ];\n }\n\n $result[(int) $row['opp_id']] = $data;\n }\n\n return $this->hydrateStages($result);\n }\n\n private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder\n {\n $qb = clone $queryBuilder;\n $qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');\n\n $qb\n ->select(...[\n 'opp.id as opp_id',\n 'opp.uuid',\n 'opp.name',\n 'opp.value',\n 'opp.currency_code',\n 'opp.close_date',\n 'opp.remotely_created_at',\n 'opp.is_closed',\n 'opp.is_won',\n ])\n ->addSelect(...[\n 'usr.uuid as owner_uuid',\n 'usr.name AS owner_name',\n 'usr.photo_path as owner_photo',\n 'usr.id AS owner_id',\n 'jt.name as owner_job',\n ])\n ->addSelect('opp.stage_id', 'opp.stage_updated_at')\n ->addSelect(...[\n 'acc.name AS acc_name',\n 'acc.is_internal as acc_is_internal',\n 'opp.stage_updated_at',\n 'acc.crm_provider_id AS acc_provider_id',\n 'opp.crm_provider_id AS opp_provider_id',\n ])\n ->addSelect('rt.business_process_id AS pipeline_id')\n\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'));\n\n return $qb;\n }\n\n /**\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws SocialAccountTokenInvalidException\n */\n private function getCrmService(Team $team): ServiceInterface\n {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n $crmService->setConfiguration($team->crm);\n if ($crmService instanceof UrlGeneratorInterface) {\n $crmService->setCrmUrlGenerator($team->crm);\n }\n\n return $crmService;\n }\n\n /**\n *\n * @return Generator<DealData>\n */\n public function getForecastData(DealsFilter $filter): Generator\n {\n $opportunities = DB::query()\n ->select([\n 'o.value',\n 'o.close_date',\n 'o.currency_code',\n 'o.is_won',\n 'o.is_closed',\n 'o.probability',\n 'o.forecast_category',\n ])\n ->from('opportunities', 'o')\n ->join('users', 'users.id', '=', 'o.user_id')\n ->join('groups', 'groups.id', '=', 'users.group_id')\n ->where('users.team_id', $filter->getTeam()->getId())\n ->where('o.close_date', '>=', $filter->getStartDate())\n ->where('o.close_date', '<=', $filter->getEndDate())\n ->where('o.currency_code', $filter->getCurrency())\n ->where('o.deleted_at', '=', null)\n ;\n\n $userUuidList = $filter->getUserUuidList();\n if (! empty($userUuidList)) {\n $userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);\n\n $opportunities->whereIn('users.uuid', $userUuidList);\n }\n\n $groupUuidList = $filter->getGroupUuidList();\n if (! empty($groupUuidList)) {\n $groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);\n\n $opportunities->whereIn('groups.uuid', $groupUuidList);\n }\n\n foreach ($opportunities->cursor() as $row) {\n yield new DealData(\n (float) $row->value,\n $row->close_date,\n ! empty($row->is_won),\n ! empty($row->is_closed),\n $row->probability ?: 0,\n $row->forecast_category ?: '',\n );\n }\n }\n\n public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection\n {\n return $user->subscriptionSets()\n ->where(static function (Eloquent\\Builder $query): void {\n $query\n ->whereNull('expired_at')\n ->orWhere('expired_at', '>=', now());\n })\n ->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n $join\n ->where('followable_type', Models\\Activity\\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)\n ->whereIn('followable_id', $opportunityIds);\n })\n ->pluck('followable_id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\DealInsights;\n\nuse Doctrine\\DBAL\\Connection;\nuse Generator;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealData;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealsFilter;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\QueryBuilder;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\Visitor\\QueryBuilderVisitorInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Services\\Crm\\IntegrationApp\\DTO\\Utils\\UrlGeneratorInterface;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Eloquent;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass DealsRepository implements DealsRepositoryInterface\n{\n private Connection $connection;\n\n private ProviderRegistry $providerRegistry;\n\n /**\n * @var QueryBuilderVisitorInterface[]\n */\n private array $visitors = [];\n\n /**\n * @param QueryBuilderVisitorInterface[] $visitors\n */\n public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])\n {\n $this->connection = $connection;\n $this->providerRegistry = $crmProviderRegistry;\n\n foreach ($visitors as $visitor) {\n $this->visitors[$visitor->getIdentifier()] = $visitor;\n }\n }\n\n public function getDeals(CriteriaInterface $criteria): array\n {\n $context = $criteria->getContext();\n $team = $context->getTeam();\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n\n $this->visit($qb, $criteria);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getDeal(Team $team, int $id): array\n {\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n $qb->andWhere('opp.id = :id')->setParameter('id', $id);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')\n ->from('crm_fields', 'f')\n ->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')\n ->where('f.crm_configuration_id = :crm')\n ->andWhere('f.object_type = :type')\n ->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')\n ->orderBy('fd.object_id', 'ASC')\n ->addOrderBy('fd.updated_at', 'ASC')\n\n ->setParameter('type', Field::OBJECT_OPPORTUNITY)\n ->setParameter('crm', $crmId)\n ;\n\n if (! empty($crmFields)) {\n $fields = array_map(fn ($value): string => '\"' . $value . '\"', $crmFields);\n $qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');\n }\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAssociative();\n }\n\n public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('COALESCE(opp.currency_code, \"' . $defaultCurrency . '\") AS currency')\n ->addSelect('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ->groupBy('currency')\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getDealActivities(CriteriaInterface $criteria): array\n {\n $qb = Activity::with(['participants', 'user'])\n ->where('opportunity_id', $criteria->getOpportunityId())\n ->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())\n ->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())\n ->orderBy($criteria->getSortBy(), $criteria->getSortDirection())\n ;\n\n // Should we filter activities by criteria? It's intended to filter deals.\n\n return $qb->get()->all();\n }\n\n public function getStages(CriteriaInterface $criteria): array\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('id', 'label', 'sequence')\n ->from('stages', 's')\n ->where('crm_configuration_id = :crm_configuration_id')\n ->andWhere('type = :type')\n ->orderBy('sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())\n ->setParameter('type', Stage::TYPE_OPPORTUNITY);\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $result[$row['id']] = [\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n public function getConfigurationStages(Configuration $configuration): Collection\n {\n return $configuration\n ->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get();\n }\n\n public function getPipelineData(Configuration $crm): array\n {\n $qb = new QueryBuilder($this->connection);\n $provider = $crm->provider;\n\n $qb\n ->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')\n ->from('stages', 's')\n ->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')\n ->where('s.crm_configuration_id = :crm_configuration_id')\n ->andWhere('s.type = :type')\n ->orderBy('bps.business_process_id', 'ASC')\n ->addOrderBy('s.sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $crm->id)\n ->setParameter('type', Stage::TYPE_OPPORTUNITY)\n ;\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];\n $result[$row['pipeline_id']][] = [\n 'value' => $value,\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n private function createQueryBuilder(string $realm): QueryBuilder\n {\n return (new QueryBuilder($this->connection))\n ->setRealm($realm)\n ->from('opportunities', 'opp')\n ->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')\n ->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')\n ->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')\n ;\n }\n\n /**\n * Applies all applicable visitors and returns the IDs of the executed ones\n *\n * @return string[]\n */\n private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array\n {\n $queryVisitors = [];\n\n foreach ($this->visitors as $visitor) {\n if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {\n $visitor->visit($queryBuilder, $criteria);\n\n $queryVisitors[] = $visitor->getIdentifier();\n }\n }\n\n return $queryVisitors;\n }\n\n private function hydrateStages(array $deals): array\n {\n foreach ($this->fetchStages(array_keys($deals)) as $stage) {\n $oppId = (int) $stage['opportunity_id'];\n\n if (! isset($deals[$oppId])) {\n continue; // or throw??!\n }\n\n $deals[$oppId]['stages'][] = [\n 'id' => $stage['stage_id'],\n 'name' => $stage['label'],\n 'enteredAt' => $stage['created_at'],\n ];\n }\n\n return $deals;\n }\n\n /**\n * @param int[] $dealIds\n */\n private function fetchStages(array $dealIds): array\n {\n if (empty($dealIds)) {\n return [];\n }\n\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')\n ->from('opportunity_stages', 'os')\n ->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')\n ->where($qb->expr()->in('os.opportunity_id', $dealIds))\n ->orderBy('os.opportunity_id', 'ASC')\n ->addOrderBy('s.created_at', 'ASC')\n ;\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array\n {\n $result = [];\n\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $data = [\n 'uuid' => RequiresUUID::toNormal($row['uuid']),\n 'name' => $row['name'],\n 'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),\n 'account' => [\n 'name' => $row['acc_name'],\n 'url' => $crmService->generateProviderUrl(\n providerId: $row['acc_provider_id'],\n objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'\n ),\n ],\n 'owner' => null,\n 'rawValue' => [\n 'amount' => (float) $row['value'],\n 'currency' => $row['currency_code'],\n ],\n 'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),\n 'openDate' => $row['remotely_created_at'] ?? null,\n 'closeDate' => $row['close_date'] ?? null,\n 'stages' => [],\n 'currentPipelineId' => $row['pipeline_id'],\n 'currentStage' => [\n 'id' => $row['stage_id'],\n 'enteredAt' => $row['stage_updated_at'],\n ],\n 'currentStageUpdatedAt' => $row['stage_updated_at'],\n 'isClosed' => (bool) $row['is_closed'],\n 'isWon' => (bool) $row['is_won'],\n ];\n\n if (isset($row['owner_uuid'])) {\n $data['owner'] = [\n 'uuid' => RequiresUUID::toNormal($row['owner_uuid']),\n 'name' => $row['owner_name'],\n 'photoUrl' => $row['owner_photo'] === null\n ? null\n : client_cdn($row['owner_photo'], $team),\n 'id' => $row['owner_id'],\n 'job' => $row['owner_job'],\n ];\n }\n\n $result[(int) $row['opp_id']] = $data;\n }\n\n return $this->hydrateStages($result);\n }\n\n private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder\n {\n $qb = clone $queryBuilder;\n $qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');\n\n $qb\n ->select(...[\n 'opp.id as opp_id',\n 'opp.uuid',\n 'opp.name',\n 'opp.value',\n 'opp.currency_code',\n 'opp.close_date',\n 'opp.remotely_created_at',\n 'opp.is_closed',\n 'opp.is_won',\n ])\n ->addSelect(...[\n 'usr.uuid as owner_uuid',\n 'usr.name AS owner_name',\n 'usr.photo_path as owner_photo',\n 'usr.id AS owner_id',\n 'jt.name as owner_job',\n ])\n ->addSelect('opp.stage_id', 'opp.stage_updated_at')\n ->addSelect(...[\n 'acc.name AS acc_name',\n 'acc.is_internal as acc_is_internal',\n 'opp.stage_updated_at',\n 'acc.crm_provider_id AS acc_provider_id',\n 'opp.crm_provider_id AS opp_provider_id',\n ])\n ->addSelect('rt.business_process_id AS pipeline_id')\n\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'));\n\n return $qb;\n }\n\n /**\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws SocialAccountTokenInvalidException\n */\n private function getCrmService(Team $team): ServiceInterface\n {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n $crmService->setConfiguration($team->crm);\n if ($crmService instanceof UrlGeneratorInterface) {\n $crmService->setCrmUrlGenerator($team->crm);\n }\n\n return $crmService;\n }\n\n /**\n *\n * @return Generator<DealData>\n */\n public function getForecastData(DealsFilter $filter): Generator\n {\n $opportunities = DB::query()\n ->select([\n 'o.value',\n 'o.close_date',\n 'o.currency_code',\n 'o.is_won',\n 'o.is_closed',\n 'o.probability',\n 'o.forecast_category',\n ])\n ->from('opportunities', 'o')\n ->join('users', 'users.id', '=', 'o.user_id')\n ->join('groups', 'groups.id', '=', 'users.group_id')\n ->where('users.team_id', $filter->getTeam()->getId())\n ->where('o.close_date', '>=', $filter->getStartDate())\n ->where('o.close_date', '<=', $filter->getEndDate())\n ->where('o.currency_code', $filter->getCurrency())\n ->where('o.deleted_at', '=', null)\n ;\n\n $userUuidList = $filter->getUserUuidList();\n if (! empty($userUuidList)) {\n $userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);\n\n $opportunities->whereIn('users.uuid', $userUuidList);\n }\n\n $groupUuidList = $filter->getGroupUuidList();\n if (! empty($groupUuidList)) {\n $groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);\n\n $opportunities->whereIn('groups.uuid', $groupUuidList);\n }\n\n foreach ($opportunities->cursor() as $row) {\n yield new DealData(\n (float) $row->value,\n $row->close_date,\n ! empty($row->is_won),\n ! empty($row->is_closed),\n $row->probability ?: 0,\n $row->forecast_category ?: '',\n );\n }\n }\n\n public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection\n {\n return $user->subscriptionSets()\n ->where(static function (Eloquent\\Builder $query): void {\n $query\n ->whereNull('expired_at')\n ->orWhere('expired_at', '>=', now());\n })\n ->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n $join\n ->where('followable_type', Models\\Activity\\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)\n ->whereIn('followable_id', $opportunityIds);\n })\n ->pluck('followable_id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.24335106,"top":0.047885075,"width":0.024268618,"height":0.024740623},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3407362332434637772
|
-4167466460282672524
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
16
7
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active Ask Jiminny reports whose expiry date has passed.
*
* @return Collection<AutomatedReport>
*/
public function getExpiredActiveAskJiminnyReports(): Collection
{
return AutomatedReport::where('status', true)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereNotNull('expires_at')
->where('expires_at', '<', now()->toDateString())
->get();
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('report_id', $report->getId())
->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])
->latest()
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Restrict a query on the automated_reports table to reports the given user is allowed to see.
*
* Matches the customer-facing audience:
* - explicit user recipients (recipients.users)
* - members of any of the report's groups (Ask Jiminny reports)
*/
private function applyUserAccessScope(Builder $query, User $user): void
{
$userId = $user->getId();
$groupId = $user->getGroupId();
$query
->where('automated_reports.team_id', $user->getTeamId())
->where(function (Builder $q) use ($userId, $groupId): void {
$q->whereJsonContains('automated_reports.recipients->users', $userId);
if ($groupId !== null) {
$q->orWhere(function (Builder $sub) use ($groupId): void {
$sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereJsonContains('automated_reports.groups', $groupId);
});
}
});
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
36
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\DealInsights;
use Doctrine\DBAL\Connection;
use Generator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Jiminny\Component\DealInsights\Forecast\DealData;
use Jiminny\Component\DealInsights\Forecast\DealsFilter;
use Jiminny\Component\DealInsights\QueryBuilder\QueryBuilder;
use Jiminny\Component\DealInsights\QueryBuilder\Visitor\QueryBuilderVisitorInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Services\Crm\IntegrationApp\DTO\Utils\UrlGeneratorInterface;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class DealsRepository implements DealsRepositoryInterface
{
private Connection $connection;
private ProviderRegistry $providerRegistry;
/**
* @var QueryBuilderVisitorInterface[]
*/
private array $visitors = [];
/**
* @param QueryBuilderVisitorInterface[] $visitors
*/
public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])
{
$this->connection = $connection;
$this->providerRegistry = $crmProviderRegistry;
foreach ($visitors as $visitor) {
$this->visitors[$visitor->getIdentifier()] = $visitor;
}
}
public function getDeals(CriteriaInterface $criteria): array
{
$context = $criteria->getContext();
$team = $context->getTeam();
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$this->visit($qb, $criteria);
return $this->execute($team, $crmService, $qb);
}
public function getDeal(Team $team, int $id): array
{
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$qb->andWhere('opp.id = :id')->setParameter('id', $id);
return $this->execute($team, $crmService, $qb);
}
public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])
{
$qb = new QueryBuilder($this->connection);
$qb
->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')
->from('crm_fields', 'f')
->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')
->where('f.crm_configuration_id = :crm')
->andWhere('f.object_type = :type')
->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')
->orderBy('fd.object_id', 'ASC')
->addOrderBy('fd.updated_at', 'ASC')
->setParameter('type', Field::OBJECT_OPPORTUNITY)
->setParameter('crm', $crmId)
;
if (! empty($crmFields)) {
$fields = array_map(fn ($value): string => '"' . $value . '"', $crmFields);
$qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');
}
return $qb->executeQuery()->fetchAllAssociative();
}
public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAssociative();
}
public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('COALESCE(opp.currency_code, "' . $defaultCurrency . '") AS currency')
->addSelect('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
->groupBy('currency')
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAllAssociative();
}
public function getDealActivities(CriteriaInterface $criteria): array
{
$qb = Activity::with(['participants', 'user'])
->where('opportunity_id', $criteria->getOpportunityId())
->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())
->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())
->orderBy($criteria->getSortBy(), $criteria->getSortDirection())
;
// Should we filter activities by criteria? It's intended to filter deals.
return $qb->get()->all();
}
public function getStages(CriteriaInterface $criteria): array
{
$qb = new QueryBuilder($this->connection);
$qb
->select('id', 'label', 'sequence')
->from('stages', 's')
->where('crm_configuration_id = :crm_configuration_id')
->andWhere('type = :type')
->orderBy('sequence', 'ASC')
->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())
->setParameter('type', Stage::TYPE_OPPORTUNITY);
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$result[$row['id']] = [
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
public function getConfigurationStages(Configuration $configuration): Collection
{
return $configuration
->stages()
->where('type', Stage::TYPE_OPPORTUNITY)
->get();
}
public function getPipelineData(Configuration $crm): array
{
$qb = new QueryBuilder($this->connection);
$provider = $crm->provider;
$qb
->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')
->from('stages', 's')
->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')
->where('s.crm_configuration_id = :crm_configuration_id')
->andWhere('s.type = :type')
->orderBy('bps.business_process_id', 'ASC')
->addOrderBy('s.sequence', 'ASC')
->setParameter('crm_configuration_id', $crm->id)
->setParameter('type', Stage::TYPE_OPPORTUNITY)
;
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];
$result[$row['pipeline_id']][] = [
'value' => $value,
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
private function createQueryBuilder(string $realm): QueryBuilder
{
return (new QueryBuilder($this->connection))
->setRealm($realm)
->from('opportunities', 'opp')
->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')
->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')
->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')
;
}
/**
* Applies all applicable visitors and returns the IDs of the executed ones
*
* @return string[]
*/
private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array
{
$queryVisitors = [];
foreach ($this->visitors as $visitor) {
if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {
$visitor->visit($queryBuilder, $criteria);
$queryVisitors[] = $visitor->getIdentifier();
}
}
return $queryVisitors;
}
private function hydrateStages(array $deals): array
{
foreach ($this->fetchStages(array_keys($deals)) as $stage) {
$oppId = (int) $stage['opportunity_id'];
if (! isset($deals[$oppId])) {
continue; // or throw??!
}
$deals[$oppId]['stages'][] = [
'id' => $stage['stage_id'],
'name' => $stage['label'],
'enteredAt' => $stage['created_at'],
];
}
return $deals;
}
/**
* @param int[] $dealIds
*/
private function fetchStages(array $dealIds): array
{
if (empty($dealIds)) {
return [];
}
$qb = new QueryBuilder($this->connection);
$qb
->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')
->from('opportunity_stages', 'os')
->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')
->where($qb->expr()->in('os.opportunity_id', $dealIds))
->orderBy('os.opportunity_id', 'ASC')
->addOrderBy('s.created_at', 'ASC')
;
return $qb->executeQuery()->fetchAllAssociative();
}
private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array
{
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$data = [
'uuid' => RequiresUUID::toNormal($row['uuid']),
'name' => $row['name'],
'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),
'account' => [
'name' => $row['acc_name'],
'url' => $crmService->generateProviderUrl(
providerId: $row['acc_provider_id'],
objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'
),
],
'owner' => null,
'rawValue' => [
'amount' => (float) $row['value'],
'currency' => $row['currency_code'],
],
'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),
'openDate' => $row['remotely_created_at'] ?? null,
'closeDate' => $row['close_date'] ?? null,
'stages' => [],
'currentPipelineId' => $row['pipeline_id'],
'currentStage' => [
'id' => $row['stage_id'],
'enteredAt' => $row['stage_updated_at'],
],
'currentStageUpdatedAt' => $row['stage_updated_at'],
'isClosed' => (bool) $row['is_closed'],
'isWon' => (bool) $row['is_won'],
];
if (isset($row['owner_uuid'])) {
$data['owner'] = [
'uuid' => RequiresUUID::toNormal($row['owner_uuid']),
'name' => $row['owner_name'],
'photoUrl' => $row['owner_photo'] === null
? null
: client_cdn($row['owner_photo'], $team),
'id' => $row['owner_id'],
'job' => $row['owner_job'],
];
}
$result[(int) $row['opp_id']] = $data;
}
return $this->hydrateStages($result);
}
private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder
{
$qb = clone $queryBuilder;
$qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');
$qb
->select(...[
'opp.id as opp_id',
'opp.uuid',
'opp.name',
'opp.value',
'opp.currency_code',
'opp.close_date',
'opp.remotely_created_at',
'opp.is_closed',
'opp.is_won',
])
->addSelect(...[
'usr.uuid as owner_uuid',
'usr.name AS owner_name',
'usr.photo_path as owner_photo',
'usr.id AS owner_id',
'jt.name as owner_job',
])
->addSelect('opp.stage_id', 'opp.stage_updated_at')
->addSelect(...[
'acc.name AS acc_name',
'acc.is_internal as acc_is_internal',
'opp.stage_updated_at',
'acc.crm_provider_id AS acc_provider_id',
'opp.crm_provider_id AS opp_provider_id',
])
->addSelect('rt.business_process_id AS pipeline_id')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'));
return $qb;
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws SocialAccountTokenInvalidException
*/
private function getCrmService(Team $team): ServiceInterface
{
$crmService = $this->providerRegistry->get($team->crm->provider);
$crmService->setConfiguration($team->crm);
if ($crmService instanceof UrlGeneratorInterface) {
$crmService->setCrmUrlGenerator($team->crm);
}
return $crmService;
}
/**
*
* @return Generator<DealData>
*/
public function getForecastData(DealsFilter $filter): Generator
{
$opportunities = DB::query()
->select([
'o.value',
'o.close_date',
'o.currency_code',
'o.is_won',
'o.is_closed',
'o.probability',
'o.forecast_category',
])
->from('opportunities', 'o')
->join('users', 'users.id', '=', 'o.user_id')
->join('groups', 'groups.id', '=', 'users.group_id')
->where('users.team_id', $filter->getTeam()->getId())
->where('o.close_date', '>=', $filter->getStartDate())
->where('o.close_date', '<=', $filter->getEndDate())
->where('o.currency_code', $filter->getCurrency())
->where('o.deleted_at', '=', null)
;
$userUuidList = $filter->getUserUuidList();
if (! empty($userUuidList)) {
$userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);
$opportunities->whereIn('users.uuid', $userUuidList);
}
$groupUuidList = $filter->getGroupUuidList();
if (! empty($groupUuidList)) {
$groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);
$opportunities->whereIn('groups.uuid', $groupUuidList);
}
foreach ($opportunities->cursor() as $row) {
yield new DealData(
(float) $row->value,
$row->close_date,
! empty($row->is_won),
! empty($row->is_closed),
$row->probability ?: 0,
$row->forecast_category ?: '',
);
}
}
public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection
{
return $user->subscriptionSets()
->where(static function (Eloquent\Builder $query): void {
$query
->whereNull('expired_at')
->orWhere('expired_at', '>=', now());
})
->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {
$join
->on('subscription_set_id', '=', 'activity_subscription_sets.id');
$join
->where('followable_type', Models\Activity\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)
->whereIn('followable_id', $opportunityIds);
})
->pluck('followable_id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
|
73211
|
NULL
|
0
|
2026-04-23T07:00:34.208506+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776927634208_m2.jpg...
|
Firefox
|
Meet - Daily - Platform — Work
|
True
|
meet.google.com/agt-teir-cwt?authuser=lukas.kovali meet.google.com/agt-teir-cwt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - Daily - Platform
Close tab
New Tab
Open Goo Meet - Daily - Platform
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
9
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Unpin Nikolay Yankov's presentation from your main screen
You can't unmute someone else's presentation
More options for Nikolay Yankov
Zoom in
Open in new window
Enter Full Screen
Pin Steliyan Georgiev to your main screen
You can't unmute someone else
More options for Steliyan Georgiev
Steliyan Georgiev
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Pin Nikolay Ivanov to your main screen
Mute Nikolay Ivanov's microphone
More options for Nikolay Ivanov
Nikolay Ivanov
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Pin Nikolay Yankov to your main screen
You can't unmute someone else
More options for Nikolay Yankov
Nikolay Yankov
4 others
4 others
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
Others might see more of your background. Click to view your full video.
10:00
AM
Daily - Platform
Daily - Platform
Audio settings
Turn off microphone
Video settings
Turn off camera
Nikolay Yankov is presenting
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Your microphone is on....
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - Daily - Platform","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.016123671,"height":-0.051875472},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.27094415,"top":1.0,"width":0.004986702,"height":-0.051875472},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.27310506,"top":1.0,"width":0.010638298,"height":-0.086193085},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Nikolay Yankov (Presenting)","depth":12,"bounds":{"left":0.30634972,"top":1.0,"width":0.059507977,"height":-0.072625756},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Nikolay Yankov (Presenting)","depth":13,"bounds":{"left":0.30634972,"top":1.0,"width":0.059507977,"height":-0.07342374},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"People","depth":15,"bounds":{"left":0.69481385,"top":1.0,"width":0.019614361,"height":-0.06424582},"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9","depth":22,"bounds":{"left":0.7081117,"top":1.0,"width":0.0023271276,"height":-0.072625756},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Take notes with Gemini","depth":14,"bounds":{"left":0.71708775,"top":1.0,"width":0.011968086,"height":-0.06424582},"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Take notes with Gemini","depth":17,"bounds":{"left":0.7184175,"top":1.0,"width":0.043550532,"height":-0.072625756},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Gemini","depth":22,"bounds":{"left":0.7330452,"top":1.0,"width":0.013464096,"height":-0.072625756},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Gemini","depth":21,"bounds":{"left":0.73204786,"top":1.0,"width":0.011303191,"height":-0.065043926},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Pop out this video More screens are more fun. Play this video while you do other things.","depth":15,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pop out this video","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"More screens are more fun. Play this video while you do other things.","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Unpin Nikolay Yankov's presentation from your main screen","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"You can't unmute someone else's presentation","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Nikolay Yankov","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Zoom in","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open in new window","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Enter Full Screen","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Pin Steliyan Georgiev to your main screen","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"You can't unmute someone else","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Steliyan Georgiev","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Steliyan Georgiev","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pop out this video More screens are more fun. Play this video while you do other things.","depth":15,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pop out this video","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"More screens are more fun. Play this video while you do other things.","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Nikolay Ivanov to your main screen","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Mute Nikolay Ivanov's microphone","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Nikolay Ivanov","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Nikolay Ivanov","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pop out this video More screens are more fun. Play this video while you do other things.","depth":15,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pop out this video","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"More screens are more fun. Play this video while you do other things.","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Nikolay Yankov to your main screen","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"You can't unmute someone else","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Nikolay Yankov","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Nikolay Yankov","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"4 others","depth":11,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 others","depth":13,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pop out this video More screens are more fun. Play this video while you do other things.","depth":15,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pop out this video","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"More screens are more fun. Play this video while you do other things.","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"You’re continuously framed","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Backgrounds and effects","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Lukas Kovalik","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Lukas Kovalik","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Others might see more of your background. Click to view your full video.","depth":14,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"10:00","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"AM","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Daily - Platform","depth":12,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Daily - Platform","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Audio settings","depth":14,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn off microphone","depth":14,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":true,"is_selected":false},{"role":"AXButton","text":"Video settings","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn off camera","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Nikolay Yankov is presenting","depth":12,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Send a reaction","depth":12,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn on captions","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Raise hand (ctrl + ⌘ + h)","depth":12,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options","depth":12,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Leave call","depth":12,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Meeting details","depth":12,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat with everyone","depth":12,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Meeting tools","depth":12,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Your microphone is on.","depth":8,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-6464745230001326344
|
-6570503918130038000
|
click
|
hybrid
|
NULL
|
Meet - Daily - Platform
Close tab
New Tab
Open Goo Meet - Daily - Platform
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
9
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Unpin Nikolay Yankov's presentation from your main screen
You can't unmute someone else's presentation
More options for Nikolay Yankov
Zoom in
Open in new window
Enter Full Screen
Pin Steliyan Georgiev to your main screen
You can't unmute someone else
More options for Steliyan Georgiev
Steliyan Georgiev
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Pin Nikolay Ivanov to your main screen
Mute Nikolay Ivanov's microphone
More options for Nikolay Ivanov
Nikolay Ivanov
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Pin Nikolay Yankov to your main screen
You can't unmute someone else
More options for Nikolay Yankov
Nikolay Yankov
4 others
4 others
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
Others might see more of your background. Click to view your full video.
10:00
AM
Daily - Platform
Daily - Platform
Audio settings
Turn off microphone
Video settings
Turn off camera
Nikolay Yankov is presenting
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Your microphone is on.
1889.FilesLateMoreJiminny... v# deal-insights-dev# engineering# frontend# general# infra-changes# jiminny-bg• people-with-copilo..8 people-with-zoom-# platform-team# platform-tickets# product launches# randomi released# sofia-office# support# thank-yous# the people of iimi..A Direct messages(3) Aneliva Angelo…Stoyan TomovNikolay Yankovf Petko KashinskiAneliya AngelovaNikolay NikolovMario Georgievg: Todor StamatovINavicateNikolay Yankov• Messagest Add canvasur FilesLukas, Я виЖ коиоз пломача направих по ВЕ,okeй nи e?[URL_WITH_CREDENTIALS] Hs_local liminny@localnost)-repori-not-generated.dlade.png©) PlannatActivityListener.php# console PRODclass AutomatedreportsRepository416 X721 A Y© DealsRepositorv.php xououc runction tindlatestderau.torrarledresu tAutomatedRenort srevort: Automatedc) ViewercuaroserviceProvider.ono> D Queue3.09.25v → Repositories4.09.25 Ivanov>Ai4.09.25 Ivanov4.09.25 Ivanov> → AutoScorina4.09.25 Ivanov>@ Calendarverm4.09.25 Ivanov4.09.25 Ivanov© AccountRepository.php4.09.25 Ivanov© ContactRepository.php© ContactRoleRepository.php4.09.25 Ivanov4.09.25 Ivanov© CrmConfigurationRepository.p23.10.25 vanov© CrmEntityRepository.php3.09.25© FieldDataRepository.php3.09.25vanov© FieldRepository.php© LayoutEntityRepository.php3.09.25 Ivanov© LayoutRepository.php3.09.25 vanov21.04.26 Kovalik© LeadRepository.php© OpportunityRepository.php73.10.25 Ivanov3.09.25© ProfileRepository.phpIvanov3.09.25Ivanov© RecordTypeFieldValuesReposi© StageRepository.php3.09.25 Ivanov© SyncBatchRepository.php3.09.25 Ivanov> C Geography3.09.25 Ivanov© ActiveStreamsRepositorv.php3.09.25Ivanov© ActivityCommentRepository.php3.09.25 Ivanov© ActivityLoaRepository.php4.09.25 Ivanov© ActivitvMessageRepository.php10.09.25 Ivanov@ActivitvMomentRepositorv.oh3.09.25 Ivanov@ActivitvProviderRepositorv.ohp3.09.25 Ivanov@ActivitvRepositorv.ohn4.09.25 Ivanov@ActivitvSearchFilterRepositorv.oh10.09.25 Ivanov@ ActivitvShareRepositorv.ohn3.09.25 Ivanov© ActivityUploadSettingRepository-i3.09.25 Ivanov23.10.25 Ivanov(C) A'[EMAIL] & console (EUpublic function getGeneratedNotSentResults: Collection(...}1 usage— 141=143public function getPaginatedUserReports(class I vmA36public function ge$qb = Activity::with(l->where colum->Wherevarel co146ReportSortReportSortDirection $sortDirectionsresuutsPerPagespage?larbon?larbonstrombatestobateoteamlos->orderyscriter*-149150_151152// Should we filter acreturn $qb->get()->all154Pstrina Snamel): LenathAwarePaginator {155 @› ic function getStagesSquerv = AutomatedRevortResult::queruo->whereNotNul column180 @ › 1c function getConfigu'automated_report_results.report_id': 197->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))›ic function getPipelir->orderßvRaw sas->select( c->with relations: "renort team'):› ate function createQue219229if SfromDate le= null && Stolate lez nulbCauenv->wheneRetween colunn: Iaenenated att ICfromlate Ctolateln.pplies all applicablei+(1 emntv(Steamtde))$|232233234return stringl]Squery->where(function ($q) use (SteamIds) {...}):if (! empty(SreportTypes)) {Squery->whereIn( ccaucomaced reporcs.cype, sreportlypes235249gee function visit (que256,3ee funation nudratestif (4 emptv(Sname)){Sauery->wherelike column:"name'.-270param int[] $dealIds23.10.25 Ivanov23.10.25 Ivanov9 AutomatedReportsRepository.php@ CallimportRepository.php23.10.25 vanov(C) CoachinaFeedhackRenasitorv nhil3.09.25 Ivanov@ CrmTemnlateFilterRenocitorv nhr 3.09.25 Ivanov→ ate function fetchstareturn Squery->paqinateSresultsPerPade. '*']ageName: 'page', Spage):Dally - Platorm• om lenInu Zs Aor 10:00.3RequestGenerateAskJiminnyReportJobTest ~+0 ..CascadeAutomated Reports LOinougnisInought tor osNow l see the security issue. Looking at the controller (lines 46-48, 77):• phpSteamIds = Srequest->has('team') ? (array) Srequest->get('team') : []:teamIds: array_map('intval', $teamIds),The Problem:Users can opass arbitrary aroup IDs via ?team(=1236teami=456 . While apolvUserAccessScone prevents cross-team accessThe teamids filter (Uines 252-258 matches against the groups JSON tield which has dual meaning:•Ask Jiminny reports: groups = access control (who can see it) + filter• Non-Ask Jiminny reports: groups = data filter only (report content filtering)Issue 2: Users Can Filter by Groups They Don't Belong ToScenario,User is in Groun A• There are Ask Jiminny reports shared with Group AUser sends request with ?teamll=999 (aroun they don't belona to)applyUserAccessScope passes (user has access via Group A)• But the additional teamids filter requires aroups contains 999• Result: User sees empty results, even though they have valid reportsThis creates a confusing UX - the filter says "Team" but acts as an additional restriction beyond access control.Iccue 2. No Validation That Ucer Relonac to Siltered GrouncUsers can probe for group existence by trvina different tean values and seeina if results chanaeRecommencationAdd validation to ensure teanids onlv contains arouns the user helonas to.•php// In controller or serviceSallowedTeamIds = Suser->getTeam()->groups ()->pluck('id')->toArray()=Steanids = arrav intercect(steanids. CallowedTeanids)Or rename the parameter to clarify it's a content filter, not an access filter, and document that users should only filter by theirown arounsIs this exploitable for unauthorized access? No - applyUserAccessScope's team id + recipients/groups check still quardsaccess. But it's a UX bug where valid reports become hidden due to overly restrictive filteringuserie in inese tism ne aedual se the res di For oiher report trhe . Corus are usek iminny reports are shared teams so it theC° Adantive0 +]W Windsurf Toams 228.5 (1604 charc A1 line hreake)Po. 4 spac...
|
NULL
|
|
73210
|
NULL
|
0
|
2026-04-23T07:00:33.301231+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776927633301_m1.jpg...
|
Firefox
|
Meet - Daily - Platform — Work
|
True
|
meet.google.com/agt-teir-cwt?authuser=lukas.kovali meet.google.com/agt-teir-cwt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - Daily - Platform
Close tab
New Tab
Open Goo Meet - Daily - Platform
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
9
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Unpin Nikolay Yankov's presentation from your main screen
You can't unmute someone else's presentation
More options for Nikolay Yankov
Zoom in
Open in new window
Enter Full Screen
Pin Steliyan Georgiev to your main screen
You can't unmute someone else
More options for Steliyan Georgiev
Steliyan Georgiev
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Pin Nikolay Ivanov to your main screen
Mute Nikolay Ivanov's microphone
More options for Nikolay Ivanov
Nikolay Ivanov
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Pin Nikolay Yankov to your main screen
You can't unmute someone else
More options for Nikolay Yankov
Nikolay Yankov
4 others
4 others
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
Others might see more of your background. Click to view your full video.
10:00
AM
Daily - Platform
Daily - Platform
Audio settings
Turn off microphone
Video settings
Turn off camera
Nikolay Yankov is presenting
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Your microphone is on....
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - Daily - Platform","depth":4,"bounds":{"left":0.0,"top":0.072222225,"width":0.033680554,"height":0.045555554},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.0013888889,"top":0.072222225,"width":0.010416667,"height":0.016666668},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.005902778,"top":0.12,"width":0.022222223,"height":0.035555556},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.0,"top":0.7977778,"width":0.033680554,"height":0.043333333},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.0,"top":0.8411111,"width":0.033680554,"height":0.038333334},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.0,"top":0.8794444,"width":0.033680554,"height":0.03888889},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.0,"top":0.91833335,"width":0.033680554,"height":0.038333334},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0,"top":0.95666665,"width":0.033680554,"height":0.043333333},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Nikolay Yankov (Presenting)","depth":12,"bounds":{"left":0.07534722,"top":0.101111114,"width":0.124305554,"height":0.022222223},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Nikolay Yankov (Presenting)","depth":13,"bounds":{"left":0.07534722,"top":0.10222222,"width":0.124305554,"height":0.020555556},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"People","depth":15,"bounds":{"left":0.88680553,"top":0.08944444,"width":0.04097222,"height":0.04},"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9","depth":22,"bounds":{"left":0.9145833,"top":0.101111114,"width":0.0048611113,"height":0.017222222},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Take notes with Gemini","depth":14,"bounds":{"left":0.93333334,"top":0.08944444,"width":0.025,"height":0.04},"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Take notes with Gemini","depth":17,"bounds":{"left":0.9361111,"top":0.101111114,"width":0.06388891,"height":0.017222222},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Gemini","depth":22,"bounds":{"left":0.96666664,"top":0.101111114,"width":0.028125,"height":0.017222222},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Gemini","depth":21,"bounds":{"left":0.96458334,"top":0.090555556,"width":0.023611112,"height":0.037777778},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Pop out this video More screens are more fun. Play this video while you do other things.","depth":15,"bounds":{"left":0.5798611,"top":0.61,"width":0.14652778,"height":0.08888889},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pop out this video","depth":17,"bounds":{"left":0.7239583,"top":0.6244444,"width":0.08090278,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"More screens are more fun. Play this video while you do other things.","depth":16,"bounds":{"left":0.7017361,"top":0.6205556,"width":0.11076389,"height":0.05666667},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Unpin Nikolay Yankov's presentation from your main screen","depth":13,"bounds":{"left":0.34618056,"top":0.5088889,"width":0.027777778,"height":0.044444446},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"You can't unmute someone else's presentation","depth":13,"bounds":{"left":0.37395832,"top":0.50666666,"width":0.030555556,"height":0.04888889},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Nikolay Yankov","depth":13,"bounds":{"left":0.4045139,"top":0.5088889,"width":0.027777778,"height":0.044444446},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Zoom in","depth":13,"bounds":{"left":0.63090277,"top":0.78333336,"width":0.027777778,"height":0.044444446},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open in new window","depth":13,"bounds":{"left":0.6642361,"top":0.78333336,"width":0.027777778,"height":0.044444446},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Enter Full Screen","depth":13,"bounds":{"left":0.69756943,"top":0.78333336,"width":0.027777778,"height":0.044444446},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Pin Steliyan Georgiev to your main screen","depth":13,"bounds":{"left":0.7607639,"top":0.25111112,"width":0.027777778,"height":0.044444446},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"You can't unmute someone else","depth":13,"bounds":{"left":0.7885417,"top":0.2488889,"width":0.030555556,"height":0.04888889},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Steliyan Georgiev","depth":13,"bounds":{"left":0.8190972,"top":0.25111112,"width":0.027777778,"height":0.044444446},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Steliyan Georgiev","depth":17,"bounds":{"left":0.753125,"top":0.36277777,"width":0.090625,"height":0.022777777},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pop out this video More screens are more fun. Play this video while you do other things.","depth":15,"bounds":{"left":0.91180557,"top":0.27611113,"width":0.08819443,"height":0.07722222},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pop out this video","depth":17,"bounds":{"left":1.0,"top":0.2911111,"width":-0.05590272,"height":0.017777778},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"More screens are more fun. Play this video while you do other things.","depth":16,"bounds":{"left":1.0,"top":0.28666666,"width":-0.03368056,"height":0.045},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Nikolay Ivanov to your main screen","depth":13,"bounds":{"left":0.8871528,"top":0.25111112,"width":0.027777778,"height":0.044444446},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Mute Nikolay Ivanov's microphone","depth":13,"bounds":{"left":0.9149306,"top":0.2488889,"width":0.030555556,"height":0.04888889},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Nikolay Ivanov","depth":13,"bounds":{"left":0.9454861,"top":0.25111112,"width":0.027777778,"height":0.044444446},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Nikolay Ivanov","depth":17,"bounds":{"left":0.87951386,"top":0.36277777,"width":0.07395833,"height":0.022777777},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pop out this video More screens are more fun. Play this video while you do other things.","depth":15,"bounds":{"left":0.78541666,"top":0.5338889,"width":0.14652778,"height":0.07722222},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pop out this video","depth":17,"bounds":{"left":0.9295139,"top":0.54888886,"width":0.07048613,"height":0.017777778},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"More screens are more fun. Play this video while you do other things.","depth":16,"bounds":{"left":0.90729165,"top":0.54444444,"width":0.09270835,"height":0.045},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Nikolay Yankov to your main screen","depth":13,"bounds":{"left":0.7607639,"top":0.5088889,"width":0.027777778,"height":0.044444446},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"You can't unmute someone else","depth":13,"bounds":{"left":0.7885417,"top":0.50666666,"width":0.030555556,"height":0.04888889},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Nikolay Yankov","depth":13,"bounds":{"left":0.8190972,"top":0.5088889,"width":0.027777778,"height":0.044444446},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Nikolay Yankov","depth":17,"bounds":{"left":0.753125,"top":0.6205556,"width":0.07673611,"height":0.022777777},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"4 others","depth":11,"bounds":{"left":0.87118053,"top":0.40888888,"width":0.11805555,"height":0.24444444},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 others","depth":13,"bounds":{"left":0.90902776,"top":0.55722225,"width":0.04236111,"height":0.022777777},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pop out this video More screens are more fun. Play this video while you do other things.","depth":15,"bounds":{"left":0.8840278,"top":0.7916667,"width":0.11597222,"height":0.07722222},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pop out this video","depth":17,"bounds":{"left":0.8107639,"top":0.8066667,"width":0.07569444,"height":0.017777778},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"More screens are more fun. Play this video while you do other things.","depth":16,"bounds":{"left":0.79131943,"top":0.80222225,"width":0.11736111,"height":0.045},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"You’re continuously framed","depth":13,"bounds":{"left":0.82256943,"top":0.7644445,"width":0.030555556,"height":0.04888889},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Backgrounds and effects","depth":13,"bounds":{"left":0.853125,"top":0.7644445,"width":0.030555556,"height":0.04888889},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Lukas Kovalik","depth":13,"bounds":{"left":0.8836806,"top":0.76666665,"width":0.027777778,"height":0.044444446},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Lukas Kovalik","depth":17,"bounds":{"left":0.753125,"top":0.87833333,"width":0.06875,"height":0.022777777},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Others might see more of your background. Click to view your full video.","depth":14,"bounds":{"left":0.96631944,"top":0.875,"width":0.018055556,"height":0.028888889},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"10:00","depth":12,"bounds":{"left":0.050347224,"top":0.9444444,"width":0.030208332,"height":0.022777777},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"AM","depth":12,"bounds":{"left":0.084027775,"top":0.9444444,"width":0.017708333,"height":0.022777777},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Daily - Platform","depth":12,"bounds":{"left":0.119097225,"top":0.9111111,"width":0.08055556,"height":0.08888888},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Daily - Platform","depth":15,"bounds":{"left":0.119097225,"top":0.9444444,"width":0.08055556,"height":0.022777777},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Audio settings","depth":14,"bounds":{"left":0.32118055,"top":0.9288889,"width":0.06111111,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn off microphone","depth":14,"bounds":{"left":0.34895834,"top":0.9288889,"width":0.033333335,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":true,"is_selected":false},{"role":"AXButton","text":"Video settings","depth":13,"bounds":{"left":0.38784721,"top":0.9288889,"width":0.06111111,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn off camera","depth":13,"bounds":{"left":0.415625,"top":0.9288889,"width":0.033333335,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Nikolay Yankov is presenting","depth":12,"bounds":{"left":0.45451388,"top":0.9288889,"width":0.03888889,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Send a reaction","depth":12,"bounds":{"left":0.49895832,"top":0.9288889,"width":0.03888889,"height":0.053333335},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn on captions","depth":13,"bounds":{"left":0.5434028,"top":0.9288889,"width":0.03888889,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Raise hand (ctrl + ⌘ + h)","depth":12,"bounds":{"left":0.58784723,"top":0.9288889,"width":0.03888889,"height":0.053333335},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options","depth":12,"bounds":{"left":0.6322917,"top":0.9288889,"width":0.025,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Leave call","depth":12,"bounds":{"left":0.6628472,"top":0.9288889,"width":0.05,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Meeting details","depth":12,"bounds":{"left":0.89166665,"top":0.9288889,"width":0.033333335,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat with everyone","depth":12,"bounds":{"left":0.925,"top":0.9288889,"width":0.033333335,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Meeting tools","depth":12,"bounds":{"left":0.9583333,"top":0.9288889,"width":0.033333335,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Your microphone is on.","depth":8,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-6464745230001326344
|
-6570503918130038000
|
click
|
hybrid
|
NULL
|
Meet - Daily - Platform
Close tab
New Tab
Open Goo Meet - Daily - Platform
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
9
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Unpin Nikolay Yankov's presentation from your main screen
You can't unmute someone else's presentation
More options for Nikolay Yankov
Zoom in
Open in new window
Enter Full Screen
Pin Steliyan Georgiev to your main screen
You can't unmute someone else
More options for Steliyan Georgiev
Steliyan Georgiev
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Pin Nikolay Ivanov to your main screen
Mute Nikolay Ivanov's microphone
More options for Nikolay Ivanov
Nikolay Ivanov
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Pin Nikolay Yankov to your main screen
You can't unmute someone else
More options for Nikolay Yankov
Nikolay Yankov
4 others
4 others
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
Others might see more of your background. Click to view your full video.
10:00
AM
Daily - Platform
Daily - Platform
Audio settings
Turn off microphone
Video settings
Turn off camera
Nikolay Yankov is presenting
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Your microphone is on.
FirefoxFileEditViewHistoryBookmarksProfilesToolsWindow→Helpmeet.google.com/agt-teir-cwt?authuser=lukas.kovalik%40jiminny.comNikolay Yankov (Presenting)§ Daily - Platform • 5 m leftA100% <478• Thu 23 Apr 10:00:369+Platform Team %.Q Search boardS23С J-92121Service-XQ AireportiF Project P.# Font AweXE Datados8009 J1-20269 1 2 J7-40352Sync opportunities without a local owner (user_id is null)~ DescriptionWe've had a discussion with @Galya Dimitrova about syncing opportunities that have no assigned Jiminny user._jd.We came up with the following plan to optimise the process a bit1.11 an opportunity has no user.jd and no owner.id (external user id), we should try to get the account user Jd and itavailable, apply to the opportunity as well. This scenario happens becaus a some CRMs do not require a mandatoryopportunity owner id.Update such opportunities retrospectively wherever possible. We should probabiy update only reiatively resentAfter point 1 is finished and updated:2. CRM Filing - Do not run analyses on opportunity that has no local user,jd. No point in analysing noone's data.3. Examine if there's a probiem with Sync Profiles. I.e.Team Data Uiteracy Academy(Prod US TeamID: 615) has many proflies, and deals with owner_jd but no user ja. It alsolooks like we do not have the correct scope to read users from their hubspot.Validate sync profiies fails with more visibility in such cases, so that we can request more scopes manually.4. Opportunities that have owner _id, but no user_id should be ignored when syncing.Subtasks# Project P© aop.dev3 Pipelines* JiminnyD [EMAIL] Dev-Ix Improve Technical StoryDetailsAssigneeNikolay IvanovAssign to meКE DORCE@ Vasi Vasiev• Quick start developmentLink this work item to your codeby including keys whenleurenen, contut obelow. Learn moreDismissDevelc onien@ Open with VS Codecreate crancr"Greate commetComponents10:00 AM | Daily - Platform→(a ArchitedGroup: Querie:CLONE - [Team insights)Filter gets resetautomaticalytJ ooa05.000Assue win reconnecuingZohoUepoycoiwwwolt[Tech Davl ImprovePop out this videoE 3y-20606Les Mils activity types notpullling inSUPPORT TICKETSDeployed1n •=®Q•..Thu 23 Apr 10:00L Al Bockmarks|Steliyan GeorgievNikolay Ivanov4 othersNikolay YankovLukas Kovalik14:55...
|
NULL
|
|
73173
|
NULL
|
0
|
2026-04-23T06:55:02.871011+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776927302871_m2.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsRepository.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
16
7
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active Ask Jiminny reports whose expiry date has passed.
*
* @return Collection<AutomatedReport>
*/
public function getExpiredActiveAskJiminnyReports(): Collection
{
return AutomatedReport::where('status', true)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereNotNull('expires_at')
->where('expires_at', '<', now()->toDateString())
->get();
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('report_id', $report->getId())
->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])
->latest()
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Restrict a query on the automated_reports table to reports the given user is allowed to see.
*
* Matches the customer-facing audience:
* - explicit user recipients (recipients.users)
* - members of any of the report's groups (Ask Jiminny reports)
*/
private function applyUserAccessScope(Builder $query, User $user): void
{
$userId = $user->getId();
$groupId = $user->getGroupId();
$query
->where('automated_reports.team_id', $user->getTeamId())
->where(function (Builder $q) use ($userId, $groupId): void {
$q->whereJsonContains('automated_reports.recipients->users', $userId);
if ($groupId !== null) {
$q->orWhere(function (Builder $sub) use ($groupId): void {
$sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereJsonContains('automated_reports.groups', $groupId);
});
}
});
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
36
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\DealInsights;
use Doctrine\DBAL\Connection;
use Generator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Jiminny\Component\DealInsights\Forecast\DealData;
use Jiminny\Component\DealInsights\Forecast\DealsFilter;
use Jiminny\Component\DealInsights\QueryBuilder\QueryBuilder;
use Jiminny\Component\DealInsights\QueryBuilder\Visitor\QueryBuilderVisitorInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Services\Crm\IntegrationApp\DTO\Utils\UrlGeneratorInterface;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class DealsRepository implements DealsRepositoryInterface
{
private Connection $connection;
private ProviderRegistry $providerRegistry;
/**
* @var QueryBuilderVisitorInterface[]
*/
private array $visitors = [];
/**
* @param QueryBuilderVisitorInterface[] $visitors
*/
public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])
{
$this->connection = $connection;
$this->providerRegistry = $crmProviderRegistry;
foreach ($visitors as $visitor) {
$this->visitors[$visitor->getIdentifier()] = $visitor;
}
}
public function getDeals(CriteriaInterface $criteria): array
{
$context = $criteria->getContext();
$team = $context->getTeam();
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$this->visit($qb, $criteria);
return $this->execute($team, $crmService, $qb);
}
public function getDeal(Team $team, int $id): array
{
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$qb->andWhere('opp.id = :id')->setParameter('id', $id);
return $this->execute($team, $crmService, $qb);
}
public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])
{
$qb = new QueryBuilder($this->connection);
$qb
->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')
->from('crm_fields', 'f')
->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')
->where('f.crm_configuration_id = :crm')
->andWhere('f.object_type = :type')
->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')
->orderBy('fd.object_id', 'ASC')
->addOrderBy('fd.updated_at', 'ASC')
->setParameter('type', Field::OBJECT_OPPORTUNITY)
->setParameter('crm', $crmId)
;
if (! empty($crmFields)) {
$fields = array_map(fn ($value): string => '"' . $value . '"', $crmFields);
$qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');
}
return $qb->executeQuery()->fetchAllAssociative();
}
public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAssociative();
}
public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('COALESCE(opp.currency_code, "' . $defaultCurrency . '") AS currency')
->addSelect('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
->groupBy('currency')
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAllAssociative();
}
public function getDealActivities(CriteriaInterface $criteria): array
{
$qb = Activity::with(['participants', 'user'])
->where('opportunity_id', $criteria->getOpportunityId())
->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())
->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())
->orderBy($criteria->getSortBy(), $criteria->getSortDirection())
;
// Should we filter activities by criteria? It's intended to filter deals.
return $qb->get()->all();
}
public function getStages(CriteriaInterface $criteria): array
{
$qb = new QueryBuilder($this->connection);
$qb
->select('id', 'label', 'sequence')
->from('stages', 's')
->where('crm_configuration_id = :crm_configuration_id')
->andWhere('type = :type')
->orderBy('sequence', 'ASC')
->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())
->setParameter('type', Stage::TYPE_OPPORTUNITY);
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$result[$row['id']] = [
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
public function getConfigurationStages(Configuration $configuration): Collection
{
return $configuration
->stages()
->where('type', Stage::TYPE_OPPORTUNITY)
->get();
}
public function getPipelineData(Configuration $crm): array
{
$qb = new QueryBuilder($this->connection);
$provider = $crm->provider;
$qb
->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')
->from('stages', 's')
->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')
->where('s.crm_configuration_id = :crm_configuration_id')
->andWhere('s.type = :type')
->orderBy('bps.business_process_id', 'ASC')
->addOrderBy('s.sequence', 'ASC')
->setParameter('crm_configuration_id', $crm->id)
->setParameter('type', Stage::TYPE_OPPORTUNITY)
;
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];
$result[$row['pipeline_id']][] = [
'value' => $value,
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
private function createQueryBuilder(string $realm): QueryBuilder
{
return (new QueryBuilder($this->connection))
->setRealm($realm)
->from('opportunities', 'opp')
->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')
->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')
->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')
;
}
/**
* Applies all applicable visitors and returns the IDs of the executed ones
*
* @return string[]
*/
private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array
{
$queryVisitors = [];
foreach ($this->visitors as $visitor) {
if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {
$visitor->visit($queryBuilder, $criteria);
$queryVisitors[] = $visitor->getIdentifier();
}
}
return $queryVisitors;
}
private function hydrateStages(array $deals): array
{
foreach ($this->fetchStages(array_keys($deals)) as $stage) {
$oppId = (int) $stage['opportunity_id'];
if (! isset($deals[$oppId])) {
continue; // or throw??!
}
$deals[$oppId]['stages'][] = [
'id' => $stage['stage_id'],
'name' => $stage['label'],
'enteredAt' => $stage['created_at'],
];
}
return $deals;
}
/**
* @param int[] $dealIds
*/
private function fetchStages(array $dealIds): array
{
if (empty($dealIds)) {
return [];
}
$qb = new QueryBuilder($this->connection);
$qb
->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')
->from('opportunity_stages', 'os')
->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')
->where($qb->expr()->in('os.opportunity_id', $dealIds))
->orderBy('os.opportunity_id', 'ASC')
->addOrderBy('s.created_at', 'ASC')
;
return $qb->executeQuery()->fetchAllAssociative();
}
private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array
{
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$data = [
'uuid' => RequiresUUID::toNormal($row['uuid']),
'name' => $row['name'],
'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),
'account' => [
'name' => $row['acc_name'],
'url' => $crmService->generateProviderUrl(
providerId: $row['acc_provider_id'],
objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'
),
],
'owner' => null,
'rawValue' => [
'amount' => (float) $row['value'],
'currency' => $row['currency_code'],
],
'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),
'openDate' => $row['remotely_created_at'] ?? null,
'closeDate' => $row['close_date'] ?? null,
'stages' => [],
'currentPipelineId' => $row['pipeline_id'],
'currentStage' => [
'id' => $row['stage_id'],
'enteredAt' => $row['stage_updated_at'],
],
'currentStageUpdatedAt' => $row['stage_updated_at'],
'isClosed' => (bool) $row['is_closed'],
'isWon' => (bool) $row['is_won'],
];
if (isset($row['owner_uuid'])) {
$data['owner'] = [
'uuid' => RequiresUUID::toNormal($row['owner_uuid']),
'name' => $row['owner_name'],
'photoUrl' => $row['owner_photo'] === null
? null
: client_cdn($row['owner_photo'], $team),
'id' => $row['owner_id'],
'job' => $row['owner_job'],
];
}
$result[(int) $row['opp_id']] = $data;
}
return $this->hydrateStages($result);
}
private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder
{
$qb = clone $queryBuilder;
$qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');
$qb
->select(...[
'opp.id as opp_id',
'opp.uuid',
'opp.name',
'opp.value',
'opp.currency_code',
'opp.close_date',
'opp.remotely_created_at',
'opp.is_closed',
'opp.is_won',
])
->addSelect(...[
'usr.uuid as owner_uuid',
'usr.name AS owner_name',
'usr.photo_path as owner_photo',
'usr.id AS owner_id',
'jt.name as owner_job',
])
->addSelect('opp.stage_id', 'opp.stage_updated_at')
->addSelect(...[
'acc.name AS acc_name',
'acc.is_internal as acc_is_internal',
'opp.stage_updated_at',
'acc.crm_provider_id AS acc_provider_id',
'opp.crm_provider_id AS opp_provider_id',
])
->addSelect('rt.business_process_id AS pipeline_id')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'));
return $qb;
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws SocialAccountTokenInvalidException
*/
private function getCrmService(Team $team): ServiceInterface
{
$crmService = $this->providerRegistry->get($team->crm->provider);
$crmService->setConfiguration($team->crm);
if ($crmService instanceof UrlGeneratorInterface) {
$crmService->setCrmUrlGenerator($team->crm);
}
return $crmService;
}
/**
*
* @return Generator<DealData>
*/
public function getForecastData(DealsFilter $filter): Generator
{
$opportunities = DB::query()
->select([
'o.value',
'o.close_date',
'o.currency_code',
'o.is_won',
'o.is_closed',
'o.probability',
'o.forecast_category',
])
->from('opportunities', 'o')
->join('users', 'users.id', '=', 'o.user_id')
->join('groups', 'groups.id', '=', 'users.group_id')
->where('users.team_id', $filter->getTeam()->getId())
->where('o.close_date', '>=', $filter->getStartDate())
->where('o.close_date', '<=', $filter->getEndDate())
->where('o.currency_code', $filter->getCurrency())
->where('o.deleted_at', '=', null)
;
$userUuidList = $filter->getUserUuidList();
if (! empty($userUuidList)) {
$userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);
$opportunities->whereIn('users.uuid', $userUuidList);
}
$groupUuidList = $filter->getGroupUuidList();
if (! empty($groupUuidList)) {
$groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);
$opportunities->whereIn('groups.uuid', $groupUuidList);
}
foreach ($opportunities->cursor() as $row) {
yield new DealData(
(float) $row->value,
$row->close_date,
! empty($row->is_won),
! empty($row->is_closed),
$row->probability ?: 0,
$row->forecast_category ?: '',
);
}
}
public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection
{
return $user->subscriptionSets()
->where(static function (Eloquent\Builder $query): void {
$query
->whereNull('expired_at')
->orWhere('expired_at', '>=', now());
})
->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {
$join
->on('subscription_set_id', '=', 'activity_subscription_sets.id');
$join
->where('followable_type', Models\Activity\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)
->whereIn('followable_id', $opportunityIds);
})
->pluck('followable_id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.25731382,"top":0.019952115,"width":0.03856383,"height":0.025538707},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20157-AJ-report-not-send-notification, menu","depth":5,"bounds":{"left":0.29587767,"top":0.019952115,"width":0.10139628,"height":0.025538707},"help_text":"Git Branch: JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.7972075,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"RequestGenerateAskJiminnyReportJobTest","depth":6,"bounds":{"left":0.8125,"top":0.019952115,"width":0.10305851,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"16","depth":4,"bounds":{"left":0.6030585,"top":0.17478053,"width":0.009640957,"height":0.015163607},"role_description":"text"},{"role":"AXStaticText","text":"7","depth":4,"bounds":{"left":0.6146942,"top":0.17478053,"width":0.0076462766,"height":0.015163607},"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.6243351,"top":0.17478053,"width":0.00731383,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.63331115,"top":0.17318435,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.640625,"top":0.17318435,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active Ask Jiminny reports whose expiry date has passed.\n *\n * @return Collection<AutomatedReport>\n */\n public function getExpiredActiveAskJiminnyReports(): Collection\n {\n return AutomatedReport::where('status', true)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereNotNull('expires_at')\n ->where('expires_at', '<', now()->toDateString())\n ->get();\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('report_id', $report->getId())\n ->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])\n ->latest()\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Restrict a query on the automated_reports table to reports the given user is allowed to see.\n *\n * Matches the customer-facing audience:\n * - explicit user recipients (recipients.users)\n * - members of any of the report's groups (Ask Jiminny reports)\n */\n private function applyUserAccessScope(Builder $query, User $user): void\n {\n $userId = $user->getId();\n $groupId = $user->getGroupId();\n\n $query\n ->where('automated_reports.team_id', $user->getTeamId())\n ->where(function (Builder $q) use ($userId, $groupId): void {\n $q->whereJsonContains('automated_reports.recipients->users', $userId);\n\n if ($groupId !== null) {\n $q->orWhere(function (Builder $sub) use ($groupId): void {\n $sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereJsonContains('automated_reports.groups', $groupId);\n });\n }\n });\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active Ask Jiminny reports whose expiry date has passed.\n *\n * @return Collection<AutomatedReport>\n */\n public function getExpiredActiveAskJiminnyReports(): Collection\n {\n return AutomatedReport::where('status', true)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereNotNull('expires_at')\n ->where('expires_at', '<', now()->toDateString())\n ->get();\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('report_id', $report->getId())\n ->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])\n ->latest()\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Restrict a query on the automated_reports table to reports the given user is allowed to see.\n *\n * Matches the customer-facing audience:\n * - explicit user recipients (recipients.users)\n * - members of any of the report's groups (Ask Jiminny reports)\n */\n private function applyUserAccessScope(Builder $query, User $user): void\n {\n $userId = $user->getId();\n $groupId = $user->getGroupId();\n\n $query\n ->where('automated_reports.team_id', $user->getTeamId())\n ->where(function (Builder $q) use ($userId, $groupId): void {\n $q->whereJsonContains('automated_reports.recipients->users', $userId);\n\n if ($groupId !== null) {\n $q->orWhere(function (Builder $sub) use ($groupId): void {\n $sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereJsonContains('automated_reports.groups', $groupId);\n });\n }\n });\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"36","depth":4,"bounds":{"left":0.69980055,"top":0.2490024,"width":0.010305851,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7117686,"top":0.24740623,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7190825,"top":0.24740623,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\DealInsights;\n\nuse Doctrine\\DBAL\\Connection;\nuse Generator;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealData;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealsFilter;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\QueryBuilder;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\Visitor\\QueryBuilderVisitorInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Services\\Crm\\IntegrationApp\\DTO\\Utils\\UrlGeneratorInterface;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Eloquent;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass DealsRepository implements DealsRepositoryInterface\n{\n private Connection $connection;\n\n private ProviderRegistry $providerRegistry;\n\n /**\n * @var QueryBuilderVisitorInterface[]\n */\n private array $visitors = [];\n\n /**\n * @param QueryBuilderVisitorInterface[] $visitors\n */\n public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])\n {\n $this->connection = $connection;\n $this->providerRegistry = $crmProviderRegistry;\n\n foreach ($visitors as $visitor) {\n $this->visitors[$visitor->getIdentifier()] = $visitor;\n }\n }\n\n public function getDeals(CriteriaInterface $criteria): array\n {\n $context = $criteria->getContext();\n $team = $context->getTeam();\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n\n $this->visit($qb, $criteria);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getDeal(Team $team, int $id): array\n {\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n $qb->andWhere('opp.id = :id')->setParameter('id', $id);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')\n ->from('crm_fields', 'f')\n ->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')\n ->where('f.crm_configuration_id = :crm')\n ->andWhere('f.object_type = :type')\n ->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')\n ->orderBy('fd.object_id', 'ASC')\n ->addOrderBy('fd.updated_at', 'ASC')\n\n ->setParameter('type', Field::OBJECT_OPPORTUNITY)\n ->setParameter('crm', $crmId)\n ;\n\n if (! empty($crmFields)) {\n $fields = array_map(fn ($value): string => '\"' . $value . '\"', $crmFields);\n $qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');\n }\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAssociative();\n }\n\n public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('COALESCE(opp.currency_code, \"' . $defaultCurrency . '\") AS currency')\n ->addSelect('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ->groupBy('currency')\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getDealActivities(CriteriaInterface $criteria): array\n {\n $qb = Activity::with(['participants', 'user'])\n ->where('opportunity_id', $criteria->getOpportunityId())\n ->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())\n ->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())\n ->orderBy($criteria->getSortBy(), $criteria->getSortDirection())\n ;\n\n // Should we filter activities by criteria? It's intended to filter deals.\n\n return $qb->get()->all();\n }\n\n public function getStages(CriteriaInterface $criteria): array\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('id', 'label', 'sequence')\n ->from('stages', 's')\n ->where('crm_configuration_id = :crm_configuration_id')\n ->andWhere('type = :type')\n ->orderBy('sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())\n ->setParameter('type', Stage::TYPE_OPPORTUNITY);\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $result[$row['id']] = [\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n public function getConfigurationStages(Configuration $configuration): Collection\n {\n return $configuration\n ->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get();\n }\n\n public function getPipelineData(Configuration $crm): array\n {\n $qb = new QueryBuilder($this->connection);\n $provider = $crm->provider;\n\n $qb\n ->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')\n ->from('stages', 's')\n ->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')\n ->where('s.crm_configuration_id = :crm_configuration_id')\n ->andWhere('s.type = :type')\n ->orderBy('bps.business_process_id', 'ASC')\n ->addOrderBy('s.sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $crm->id)\n ->setParameter('type', Stage::TYPE_OPPORTUNITY)\n ;\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];\n $result[$row['pipeline_id']][] = [\n 'value' => $value,\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n private function createQueryBuilder(string $realm): QueryBuilder\n {\n return (new QueryBuilder($this->connection))\n ->setRealm($realm)\n ->from('opportunities', 'opp')\n ->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')\n ->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')\n ->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')\n ;\n }\n\n /**\n * Applies all applicable visitors and returns the IDs of the executed ones\n *\n * @return string[]\n */\n private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array\n {\n $queryVisitors = [];\n\n foreach ($this->visitors as $visitor) {\n if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {\n $visitor->visit($queryBuilder, $criteria);\n\n $queryVisitors[] = $visitor->getIdentifier();\n }\n }\n\n return $queryVisitors;\n }\n\n private function hydrateStages(array $deals): array\n {\n foreach ($this->fetchStages(array_keys($deals)) as $stage) {\n $oppId = (int) $stage['opportunity_id'];\n\n if (! isset($deals[$oppId])) {\n continue; // or throw??!\n }\n\n $deals[$oppId]['stages'][] = [\n 'id' => $stage['stage_id'],\n 'name' => $stage['label'],\n 'enteredAt' => $stage['created_at'],\n ];\n }\n\n return $deals;\n }\n\n /**\n * @param int[] $dealIds\n */\n private function fetchStages(array $dealIds): array\n {\n if (empty($dealIds)) {\n return [];\n }\n\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')\n ->from('opportunity_stages', 'os')\n ->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')\n ->where($qb->expr()->in('os.opportunity_id', $dealIds))\n ->orderBy('os.opportunity_id', 'ASC')\n ->addOrderBy('s.created_at', 'ASC')\n ;\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array\n {\n $result = [];\n\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $data = [\n 'uuid' => RequiresUUID::toNormal($row['uuid']),\n 'name' => $row['name'],\n 'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),\n 'account' => [\n 'name' => $row['acc_name'],\n 'url' => $crmService->generateProviderUrl(\n providerId: $row['acc_provider_id'],\n objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'\n ),\n ],\n 'owner' => null,\n 'rawValue' => [\n 'amount' => (float) $row['value'],\n 'currency' => $row['currency_code'],\n ],\n 'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),\n 'openDate' => $row['remotely_created_at'] ?? null,\n 'closeDate' => $row['close_date'] ?? null,\n 'stages' => [],\n 'currentPipelineId' => $row['pipeline_id'],\n 'currentStage' => [\n 'id' => $row['stage_id'],\n 'enteredAt' => $row['stage_updated_at'],\n ],\n 'currentStageUpdatedAt' => $row['stage_updated_at'],\n 'isClosed' => (bool) $row['is_closed'],\n 'isWon' => (bool) $row['is_won'],\n ];\n\n if (isset($row['owner_uuid'])) {\n $data['owner'] = [\n 'uuid' => RequiresUUID::toNormal($row['owner_uuid']),\n 'name' => $row['owner_name'],\n 'photoUrl' => $row['owner_photo'] === null\n ? null\n : client_cdn($row['owner_photo'], $team),\n 'id' => $row['owner_id'],\n 'job' => $row['owner_job'],\n ];\n }\n\n $result[(int) $row['opp_id']] = $data;\n }\n\n return $this->hydrateStages($result);\n }\n\n private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder\n {\n $qb = clone $queryBuilder;\n $qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');\n\n $qb\n ->select(...[\n 'opp.id as opp_id',\n 'opp.uuid',\n 'opp.name',\n 'opp.value',\n 'opp.currency_code',\n 'opp.close_date',\n 'opp.remotely_created_at',\n 'opp.is_closed',\n 'opp.is_won',\n ])\n ->addSelect(...[\n 'usr.uuid as owner_uuid',\n 'usr.name AS owner_name',\n 'usr.photo_path as owner_photo',\n 'usr.id AS owner_id',\n 'jt.name as owner_job',\n ])\n ->addSelect('opp.stage_id', 'opp.stage_updated_at')\n ->addSelect(...[\n 'acc.name AS acc_name',\n 'acc.is_internal as acc_is_internal',\n 'opp.stage_updated_at',\n 'acc.crm_provider_id AS acc_provider_id',\n 'opp.crm_provider_id AS opp_provider_id',\n ])\n ->addSelect('rt.business_process_id AS pipeline_id')\n\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'));\n\n return $qb;\n }\n\n /**\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws SocialAccountTokenInvalidException\n */\n private function getCrmService(Team $team): ServiceInterface\n {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n $crmService->setConfiguration($team->crm);\n if ($crmService instanceof UrlGeneratorInterface) {\n $crmService->setCrmUrlGenerator($team->crm);\n }\n\n return $crmService;\n }\n\n /**\n *\n * @return Generator<DealData>\n */\n public function getForecastData(DealsFilter $filter): Generator\n {\n $opportunities = DB::query()\n ->select([\n 'o.value',\n 'o.close_date',\n 'o.currency_code',\n 'o.is_won',\n 'o.is_closed',\n 'o.probability',\n 'o.forecast_category',\n ])\n ->from('opportunities', 'o')\n ->join('users', 'users.id', '=', 'o.user_id')\n ->join('groups', 'groups.id', '=', 'users.group_id')\n ->where('users.team_id', $filter->getTeam()->getId())\n ->where('o.close_date', '>=', $filter->getStartDate())\n ->where('o.close_date', '<=', $filter->getEndDate())\n ->where('o.currency_code', $filter->getCurrency())\n ->where('o.deleted_at', '=', null)\n ;\n\n $userUuidList = $filter->getUserUuidList();\n if (! empty($userUuidList)) {\n $userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);\n\n $opportunities->whereIn('users.uuid', $userUuidList);\n }\n\n $groupUuidList = $filter->getGroupUuidList();\n if (! empty($groupUuidList)) {\n $groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);\n\n $opportunities->whereIn('groups.uuid', $groupUuidList);\n }\n\n foreach ($opportunities->cursor() as $row) {\n yield new DealData(\n (float) $row->value,\n $row->close_date,\n ! empty($row->is_won),\n ! empty($row->is_closed),\n $row->probability ?: 0,\n $row->forecast_category ?: '',\n );\n }\n }\n\n public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection\n {\n return $user->subscriptionSets()\n ->where(static function (Eloquent\\Builder $query): void {\n $query\n ->whereNull('expired_at')\n ->orWhere('expired_at', '>=', now());\n })\n ->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n $join\n ->where('followable_type', Models\\Activity\\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)\n ->whereIn('followable_id', $opportunityIds);\n })\n ->pluck('followable_id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\DealInsights;\n\nuse Doctrine\\DBAL\\Connection;\nuse Generator;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealData;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealsFilter;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\QueryBuilder;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\Visitor\\QueryBuilderVisitorInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Services\\Crm\\IntegrationApp\\DTO\\Utils\\UrlGeneratorInterface;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Eloquent;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass DealsRepository implements DealsRepositoryInterface\n{\n private Connection $connection;\n\n private ProviderRegistry $providerRegistry;\n\n /**\n * @var QueryBuilderVisitorInterface[]\n */\n private array $visitors = [];\n\n /**\n * @param QueryBuilderVisitorInterface[] $visitors\n */\n public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])\n {\n $this->connection = $connection;\n $this->providerRegistry = $crmProviderRegistry;\n\n foreach ($visitors as $visitor) {\n $this->visitors[$visitor->getIdentifier()] = $visitor;\n }\n }\n\n public function getDeals(CriteriaInterface $criteria): array\n {\n $context = $criteria->getContext();\n $team = $context->getTeam();\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n\n $this->visit($qb, $criteria);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getDeal(Team $team, int $id): array\n {\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n $qb->andWhere('opp.id = :id')->setParameter('id', $id);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')\n ->from('crm_fields', 'f')\n ->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')\n ->where('f.crm_configuration_id = :crm')\n ->andWhere('f.object_type = :type')\n ->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')\n ->orderBy('fd.object_id', 'ASC')\n ->addOrderBy('fd.updated_at', 'ASC')\n\n ->setParameter('type', Field::OBJECT_OPPORTUNITY)\n ->setParameter('crm', $crmId)\n ;\n\n if (! empty($crmFields)) {\n $fields = array_map(fn ($value): string => '\"' . $value . '\"', $crmFields);\n $qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');\n }\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAssociative();\n }\n\n public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('COALESCE(opp.currency_code, \"' . $defaultCurrency . '\") AS currency')\n ->addSelect('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ->groupBy('currency')\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getDealActivities(CriteriaInterface $criteria): array\n {\n $qb = Activity::with(['participants', 'user'])\n ->where('opportunity_id', $criteria->getOpportunityId())\n ->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())\n ->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())\n ->orderBy($criteria->getSortBy(), $criteria->getSortDirection())\n ;\n\n // Should we filter activities by criteria? It's intended to filter deals.\n\n return $qb->get()->all();\n }\n\n public function getStages(CriteriaInterface $criteria): array\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('id', 'label', 'sequence')\n ->from('stages', 's')\n ->where('crm_configuration_id = :crm_configuration_id')\n ->andWhere('type = :type')\n ->orderBy('sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())\n ->setParameter('type', Stage::TYPE_OPPORTUNITY);\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $result[$row['id']] = [\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n public function getConfigurationStages(Configuration $configuration): Collection\n {\n return $configuration\n ->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get();\n }\n\n public function getPipelineData(Configuration $crm): array\n {\n $qb = new QueryBuilder($this->connection);\n $provider = $crm->provider;\n\n $qb\n ->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')\n ->from('stages', 's')\n ->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')\n ->where('s.crm_configuration_id = :crm_configuration_id')\n ->andWhere('s.type = :type')\n ->orderBy('bps.business_process_id', 'ASC')\n ->addOrderBy('s.sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $crm->id)\n ->setParameter('type', Stage::TYPE_OPPORTUNITY)\n ;\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];\n $result[$row['pipeline_id']][] = [\n 'value' => $value,\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n private function createQueryBuilder(string $realm): QueryBuilder\n {\n return (new QueryBuilder($this->connection))\n ->setRealm($realm)\n ->from('opportunities', 'opp')\n ->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')\n ->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')\n ->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')\n ;\n }\n\n /**\n * Applies all applicable visitors and returns the IDs of the executed ones\n *\n * @return string[]\n */\n private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array\n {\n $queryVisitors = [];\n\n foreach ($this->visitors as $visitor) {\n if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {\n $visitor->visit($queryBuilder, $criteria);\n\n $queryVisitors[] = $visitor->getIdentifier();\n }\n }\n\n return $queryVisitors;\n }\n\n private function hydrateStages(array $deals): array\n {\n foreach ($this->fetchStages(array_keys($deals)) as $stage) {\n $oppId = (int) $stage['opportunity_id'];\n\n if (! isset($deals[$oppId])) {\n continue; // or throw??!\n }\n\n $deals[$oppId]['stages'][] = [\n 'id' => $stage['stage_id'],\n 'name' => $stage['label'],\n 'enteredAt' => $stage['created_at'],\n ];\n }\n\n return $deals;\n }\n\n /**\n * @param int[] $dealIds\n */\n private function fetchStages(array $dealIds): array\n {\n if (empty($dealIds)) {\n return [];\n }\n\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')\n ->from('opportunity_stages', 'os')\n ->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')\n ->where($qb->expr()->in('os.opportunity_id', $dealIds))\n ->orderBy('os.opportunity_id', 'ASC')\n ->addOrderBy('s.created_at', 'ASC')\n ;\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array\n {\n $result = [];\n\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $data = [\n 'uuid' => RequiresUUID::toNormal($row['uuid']),\n 'name' => $row['name'],\n 'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),\n 'account' => [\n 'name' => $row['acc_name'],\n 'url' => $crmService->generateProviderUrl(\n providerId: $row['acc_provider_id'],\n objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'\n ),\n ],\n 'owner' => null,\n 'rawValue' => [\n 'amount' => (float) $row['value'],\n 'currency' => $row['currency_code'],\n ],\n 'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),\n 'openDate' => $row['remotely_created_at'] ?? null,\n 'closeDate' => $row['close_date'] ?? null,\n 'stages' => [],\n 'currentPipelineId' => $row['pipeline_id'],\n 'currentStage' => [\n 'id' => $row['stage_id'],\n 'enteredAt' => $row['stage_updated_at'],\n ],\n 'currentStageUpdatedAt' => $row['stage_updated_at'],\n 'isClosed' => (bool) $row['is_closed'],\n 'isWon' => (bool) $row['is_won'],\n ];\n\n if (isset($row['owner_uuid'])) {\n $data['owner'] = [\n 'uuid' => RequiresUUID::toNormal($row['owner_uuid']),\n 'name' => $row['owner_name'],\n 'photoUrl' => $row['owner_photo'] === null\n ? null\n : client_cdn($row['owner_photo'], $team),\n 'id' => $row['owner_id'],\n 'job' => $row['owner_job'],\n ];\n }\n\n $result[(int) $row['opp_id']] = $data;\n }\n\n return $this->hydrateStages($result);\n }\n\n private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder\n {\n $qb = clone $queryBuilder;\n $qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');\n\n $qb\n ->select(...[\n 'opp.id as opp_id',\n 'opp.uuid',\n 'opp.name',\n 'opp.value',\n 'opp.currency_code',\n 'opp.close_date',\n 'opp.remotely_created_at',\n 'opp.is_closed',\n 'opp.is_won',\n ])\n ->addSelect(...[\n 'usr.uuid as owner_uuid',\n 'usr.name AS owner_name',\n 'usr.photo_path as owner_photo',\n 'usr.id AS owner_id',\n 'jt.name as owner_job',\n ])\n ->addSelect('opp.stage_id', 'opp.stage_updated_at')\n ->addSelect(...[\n 'acc.name AS acc_name',\n 'acc.is_internal as acc_is_internal',\n 'opp.stage_updated_at',\n 'acc.crm_provider_id AS acc_provider_id',\n 'opp.crm_provider_id AS opp_provider_id',\n ])\n ->addSelect('rt.business_process_id AS pipeline_id')\n\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'));\n\n return $qb;\n }\n\n /**\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws SocialAccountTokenInvalidException\n */\n private function getCrmService(Team $team): ServiceInterface\n {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n $crmService->setConfiguration($team->crm);\n if ($crmService instanceof UrlGeneratorInterface) {\n $crmService->setCrmUrlGenerator($team->crm);\n }\n\n return $crmService;\n }\n\n /**\n *\n * @return Generator<DealData>\n */\n public function getForecastData(DealsFilter $filter): Generator\n {\n $opportunities = DB::query()\n ->select([\n 'o.value',\n 'o.close_date',\n 'o.currency_code',\n 'o.is_won',\n 'o.is_closed',\n 'o.probability',\n 'o.forecast_category',\n ])\n ->from('opportunities', 'o')\n ->join('users', 'users.id', '=', 'o.user_id')\n ->join('groups', 'groups.id', '=', 'users.group_id')\n ->where('users.team_id', $filter->getTeam()->getId())\n ->where('o.close_date', '>=', $filter->getStartDate())\n ->where('o.close_date', '<=', $filter->getEndDate())\n ->where('o.currency_code', $filter->getCurrency())\n ->where('o.deleted_at', '=', null)\n ;\n\n $userUuidList = $filter->getUserUuidList();\n if (! empty($userUuidList)) {\n $userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);\n\n $opportunities->whereIn('users.uuid', $userUuidList);\n }\n\n $groupUuidList = $filter->getGroupUuidList();\n if (! empty($groupUuidList)) {\n $groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);\n\n $opportunities->whereIn('groups.uuid', $groupUuidList);\n }\n\n foreach ($opportunities->cursor() as $row) {\n yield new DealData(\n (float) $row->value,\n $row->close_date,\n ! empty($row->is_won),\n ! empty($row->is_closed),\n $row->probability ?: 0,\n $row->forecast_category ?: '',\n );\n }\n }\n\n public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection\n {\n return $user->subscriptionSets()\n ->where(static function (Eloquent\\Builder $query): void {\n $query\n ->whereNull('expired_at')\n ->orWhere('expired_at', '>=', now());\n })\n ->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n $join\n ->where('followable_type', Models\\Activity\\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)\n ->whereIn('followable_id', $opportunityIds);\n })\n ->pluck('followable_id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.24335106,"top":0.047885075,"width":0.024268618,"height":0.024740623},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3407362332434637772
|
-4167466460282672524
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
16
7
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active Ask Jiminny reports whose expiry date has passed.
*
* @return Collection<AutomatedReport>
*/
public function getExpiredActiveAskJiminnyReports(): Collection
{
return AutomatedReport::where('status', true)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereNotNull('expires_at')
->where('expires_at', '<', now()->toDateString())
->get();
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('report_id', $report->getId())
->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])
->latest()
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Restrict a query on the automated_reports table to reports the given user is allowed to see.
*
* Matches the customer-facing audience:
* - explicit user recipients (recipients.users)
* - members of any of the report's groups (Ask Jiminny reports)
*/
private function applyUserAccessScope(Builder $query, User $user): void
{
$userId = $user->getId();
$groupId = $user->getGroupId();
$query
->where('automated_reports.team_id', $user->getTeamId())
->where(function (Builder $q) use ($userId, $groupId): void {
$q->whereJsonContains('automated_reports.recipients->users', $userId);
if ($groupId !== null) {
$q->orWhere(function (Builder $sub) use ($groupId): void {
$sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereJsonContains('automated_reports.groups', $groupId);
});
}
});
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
36
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\DealInsights;
use Doctrine\DBAL\Connection;
use Generator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Jiminny\Component\DealInsights\Forecast\DealData;
use Jiminny\Component\DealInsights\Forecast\DealsFilter;
use Jiminny\Component\DealInsights\QueryBuilder\QueryBuilder;
use Jiminny\Component\DealInsights\QueryBuilder\Visitor\QueryBuilderVisitorInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Services\Crm\IntegrationApp\DTO\Utils\UrlGeneratorInterface;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class DealsRepository implements DealsRepositoryInterface
{
private Connection $connection;
private ProviderRegistry $providerRegistry;
/**
* @var QueryBuilderVisitorInterface[]
*/
private array $visitors = [];
/**
* @param QueryBuilderVisitorInterface[] $visitors
*/
public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])
{
$this->connection = $connection;
$this->providerRegistry = $crmProviderRegistry;
foreach ($visitors as $visitor) {
$this->visitors[$visitor->getIdentifier()] = $visitor;
}
}
public function getDeals(CriteriaInterface $criteria): array
{
$context = $criteria->getContext();
$team = $context->getTeam();
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$this->visit($qb, $criteria);
return $this->execute($team, $crmService, $qb);
}
public function getDeal(Team $team, int $id): array
{
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$qb->andWhere('opp.id = :id')->setParameter('id', $id);
return $this->execute($team, $crmService, $qb);
}
public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])
{
$qb = new QueryBuilder($this->connection);
$qb
->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')
->from('crm_fields', 'f')
->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')
->where('f.crm_configuration_id = :crm')
->andWhere('f.object_type = :type')
->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')
->orderBy('fd.object_id', 'ASC')
->addOrderBy('fd.updated_at', 'ASC')
->setParameter('type', Field::OBJECT_OPPORTUNITY)
->setParameter('crm', $crmId)
;
if (! empty($crmFields)) {
$fields = array_map(fn ($value): string => '"' . $value . '"', $crmFields);
$qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');
}
return $qb->executeQuery()->fetchAllAssociative();
}
public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAssociative();
}
public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('COALESCE(opp.currency_code, "' . $defaultCurrency . '") AS currency')
->addSelect('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
->groupBy('currency')
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAllAssociative();
}
public function getDealActivities(CriteriaInterface $criteria): array
{
$qb = Activity::with(['participants', 'user'])
->where('opportunity_id', $criteria->getOpportunityId())
->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())
->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())
->orderBy($criteria->getSortBy(), $criteria->getSortDirection())
;
// Should we filter activities by criteria? It's intended to filter deals.
return $qb->get()->all();
}
public function getStages(CriteriaInterface $criteria): array
{
$qb = new QueryBuilder($this->connection);
$qb
->select('id', 'label', 'sequence')
->from('stages', 's')
->where('crm_configuration_id = :crm_configuration_id')
->andWhere('type = :type')
->orderBy('sequence', 'ASC')
->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())
->setParameter('type', Stage::TYPE_OPPORTUNITY);
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$result[$row['id']] = [
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
public function getConfigurationStages(Configuration $configuration): Collection
{
return $configuration
->stages()
->where('type', Stage::TYPE_OPPORTUNITY)
->get();
}
public function getPipelineData(Configuration $crm): array
{
$qb = new QueryBuilder($this->connection);
$provider = $crm->provider;
$qb
->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')
->from('stages', 's')
->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')
->where('s.crm_configuration_id = :crm_configuration_id')
->andWhere('s.type = :type')
->orderBy('bps.business_process_id', 'ASC')
->addOrderBy('s.sequence', 'ASC')
->setParameter('crm_configuration_id', $crm->id)
->setParameter('type', Stage::TYPE_OPPORTUNITY)
;
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];
$result[$row['pipeline_id']][] = [
'value' => $value,
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
private function createQueryBuilder(string $realm): QueryBuilder
{
return (new QueryBuilder($this->connection))
->setRealm($realm)
->from('opportunities', 'opp')
->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')
->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')
->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')
;
}
/**
* Applies all applicable visitors and returns the IDs of the executed ones
*
* @return string[]
*/
private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array
{
$queryVisitors = [];
foreach ($this->visitors as $visitor) {
if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {
$visitor->visit($queryBuilder, $criteria);
$queryVisitors[] = $visitor->getIdentifier();
}
}
return $queryVisitors;
}
private function hydrateStages(array $deals): array
{
foreach ($this->fetchStages(array_keys($deals)) as $stage) {
$oppId = (int) $stage['opportunity_id'];
if (! isset($deals[$oppId])) {
continue; // or throw??!
}
$deals[$oppId]['stages'][] = [
'id' => $stage['stage_id'],
'name' => $stage['label'],
'enteredAt' => $stage['created_at'],
];
}
return $deals;
}
/**
* @param int[] $dealIds
*/
private function fetchStages(array $dealIds): array
{
if (empty($dealIds)) {
return [];
}
$qb = new QueryBuilder($this->connection);
$qb
->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')
->from('opportunity_stages', 'os')
->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')
->where($qb->expr()->in('os.opportunity_id', $dealIds))
->orderBy('os.opportunity_id', 'ASC')
->addOrderBy('s.created_at', 'ASC')
;
return $qb->executeQuery()->fetchAllAssociative();
}
private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array
{
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$data = [
'uuid' => RequiresUUID::toNormal($row['uuid']),
'name' => $row['name'],
'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),
'account' => [
'name' => $row['acc_name'],
'url' => $crmService->generateProviderUrl(
providerId: $row['acc_provider_id'],
objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'
),
],
'owner' => null,
'rawValue' => [
'amount' => (float) $row['value'],
'currency' => $row['currency_code'],
],
'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),
'openDate' => $row['remotely_created_at'] ?? null,
'closeDate' => $row['close_date'] ?? null,
'stages' => [],
'currentPipelineId' => $row['pipeline_id'],
'currentStage' => [
'id' => $row['stage_id'],
'enteredAt' => $row['stage_updated_at'],
],
'currentStageUpdatedAt' => $row['stage_updated_at'],
'isClosed' => (bool) $row['is_closed'],
'isWon' => (bool) $row['is_won'],
];
if (isset($row['owner_uuid'])) {
$data['owner'] = [
'uuid' => RequiresUUID::toNormal($row['owner_uuid']),
'name' => $row['owner_name'],
'photoUrl' => $row['owner_photo'] === null
? null
: client_cdn($row['owner_photo'], $team),
'id' => $row['owner_id'],
'job' => $row['owner_job'],
];
}
$result[(int) $row['opp_id']] = $data;
}
return $this->hydrateStages($result);
}
private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder
{
$qb = clone $queryBuilder;
$qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');
$qb
->select(...[
'opp.id as opp_id',
'opp.uuid',
'opp.name',
'opp.value',
'opp.currency_code',
'opp.close_date',
'opp.remotely_created_at',
'opp.is_closed',
'opp.is_won',
])
->addSelect(...[
'usr.uuid as owner_uuid',
'usr.name AS owner_name',
'usr.photo_path as owner_photo',
'usr.id AS owner_id',
'jt.name as owner_job',
])
->addSelect('opp.stage_id', 'opp.stage_updated_at')
->addSelect(...[
'acc.name AS acc_name',
'acc.is_internal as acc_is_internal',
'opp.stage_updated_at',
'acc.crm_provider_id AS acc_provider_id',
'opp.crm_provider_id AS opp_provider_id',
])
->addSelect('rt.business_process_id AS pipeline_id')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'));
return $qb;
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws SocialAccountTokenInvalidException
*/
private function getCrmService(Team $team): ServiceInterface
{
$crmService = $this->providerRegistry->get($team->crm->provider);
$crmService->setConfiguration($team->crm);
if ($crmService instanceof UrlGeneratorInterface) {
$crmService->setCrmUrlGenerator($team->crm);
}
return $crmService;
}
/**
*
* @return Generator<DealData>
*/
public function getForecastData(DealsFilter $filter): Generator
{
$opportunities = DB::query()
->select([
'o.value',
'o.close_date',
'o.currency_code',
'o.is_won',
'o.is_closed',
'o.probability',
'o.forecast_category',
])
->from('opportunities', 'o')
->join('users', 'users.id', '=', 'o.user_id')
->join('groups', 'groups.id', '=', 'users.group_id')
->where('users.team_id', $filter->getTeam()->getId())
->where('o.close_date', '>=', $filter->getStartDate())
->where('o.close_date', '<=', $filter->getEndDate())
->where('o.currency_code', $filter->getCurrency())
->where('o.deleted_at', '=', null)
;
$userUuidList = $filter->getUserUuidList();
if (! empty($userUuidList)) {
$userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);
$opportunities->whereIn('users.uuid', $userUuidList);
}
$groupUuidList = $filter->getGroupUuidList();
if (! empty($groupUuidList)) {
$groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);
$opportunities->whereIn('groups.uuid', $groupUuidList);
}
foreach ($opportunities->cursor() as $row) {
yield new DealData(
(float) $row->value,
$row->close_date,
! empty($row->is_won),
! empty($row->is_closed),
$row->probability ?: 0,
$row->forecast_category ?: '',
);
}
}
public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection
{
return $user->subscriptionSets()
->where(static function (Eloquent\Builder $query): void {
$query
->whereNull('expired_at')
->orWhere('expired_at', '>=', now());
})
->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {
$join
->on('subscription_set_id', '=', 'activity_subscription_sets.id');
$join
->where('followable_type', Models\Activity\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)
->whereIn('followable_id', $opportunityIds);
})
->pluck('followable_id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
73163
|
|
73172
|
NULL
|
0
|
2026-04-23T06:55:01.184785+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776927301184_m1.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsRepository.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
16
7
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active Ask Jiminny reports whose expiry date has passed.
*
* @return Collection<AutomatedReport>
*/
public function getExpiredActiveAskJiminnyReports(): Collection
{
return AutomatedReport::where('status', true)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereNotNull('expires_at')
->where('expires_at', '<', now()->toDateString())
->get();
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('report_id', $report->getId())
->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])
->latest()
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Restrict a query on the automated_reports table to reports the given user is allowed to see.
*
* Matches the customer-facing audience:
* - explicit user recipients (recipients.users)
* - members of any of the report's groups (Ask Jiminny reports)
*/
private function applyUserAccessScope(Builder $query, User $user): void
{
$userId = $user->getId();
$groupId = $user->getGroupId();
$query
->where('automated_reports.team_id', $user->getTeamId())
->where(function (Builder $q) use ($userId, $groupId): void {
$q->whereJsonContains('automated_reports.recipients->users', $userId);
if ($groupId !== null) {
$q->orWhere(function (Builder $sub) use ($groupId): void {
$sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereJsonContains('automated_reports.groups', $groupId);
});
}
});
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
36
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\DealInsights;
use Doctrine\DBAL\Connection;
use Generator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Jiminny\Component\DealInsights\Forecast\DealData;
use Jiminny\Component\DealInsights\Forecast\DealsFilter;
use Jiminny\Component\DealInsights\QueryBuilder\QueryBuilder;
use Jiminny\Component\DealInsights\QueryBuilder\Visitor\QueryBuilderVisitorInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Services\Crm\IntegrationApp\DTO\Utils\UrlGeneratorInterface;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class DealsRepository implements DealsRepositoryInterface
{
private Connection $connection;
private ProviderRegistry $providerRegistry;
/**
* @var QueryBuilderVisitorInterface[]
*/
private array $visitors = [];
/**
* @param QueryBuilderVisitorInterface[] $visitors
*/
public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])
{
$this->connection = $connection;
$this->providerRegistry = $crmProviderRegistry;
foreach ($visitors as $visitor) {
$this->visitors[$visitor->getIdentifier()] = $visitor;
}
}
public function getDeals(CriteriaInterface $criteria): array
{
$context = $criteria->getContext();
$team = $context->getTeam();
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$this->visit($qb, $criteria);
return $this->execute($team, $crmService, $qb);
}
public function getDeal(Team $team, int $id): array
{
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$qb->andWhere('opp.id = :id')->setParameter('id', $id);
return $this->execute($team, $crmService, $qb);
}
public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])
{
$qb = new QueryBuilder($this->connection);
$qb
->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')
->from('crm_fields', 'f')
->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')
->where('f.crm_configuration_id = :crm')
->andWhere('f.object_type = :type')
->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')
->orderBy('fd.object_id', 'ASC')
->addOrderBy('fd.updated_at', 'ASC')
->setParameter('type', Field::OBJECT_OPPORTUNITY)
->setParameter('crm', $crmId)
;
if (! empty($crmFields)) {
$fields = array_map(fn ($value): string => '"' . $value . '"', $crmFields);
$qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');
}
return $qb->executeQuery()->fetchAllAssociative();
}
public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAssociative();
}
public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('COALESCE(opp.currency_code, "' . $defaultCurrency . '") AS currency')
->addSelect('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
->groupBy('currency')
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAllAssociative();
}
public function getDealActivities(CriteriaInterface $criteria): array
{
$qb = Activity::with(['participants', 'user'])
->where('opportunity_id', $criteria->getOpportunityId())
->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())
->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())
->orderBy($criteria->getSortBy(), $criteria->getSortDirection())
;
// Should we filter activities by criteria? It's intended to filter deals.
return $qb->get()->all();
}
public function getStages(CriteriaInterface $criteria): array
{
$qb = new QueryBuilder($this->connection);
$qb
->select('id', 'label', 'sequence')
->from('stages', 's')
->where('crm_configuration_id = :crm_configuration_id')
->andWhere('type = :type')
->orderBy('sequence', 'ASC')
->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())
->setParameter('type', Stage::TYPE_OPPORTUNITY);
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$result[$row['id']] = [
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
public function getConfigurationStages(Configuration $configuration): Collection
{
return $configuration
->stages()
->where('type', Stage::TYPE_OPPORTUNITY)
->get();
}
public function getPipelineData(Configuration $crm): array
{
$qb = new QueryBuilder($this->connection);
$provider = $crm->provider;
$qb
->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')
->from('stages', 's')
->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')
->where('s.crm_configuration_id = :crm_configuration_id')
->andWhere('s.type = :type')
->orderBy('bps.business_process_id', 'ASC')
->addOrderBy('s.sequence', 'ASC')
->setParameter('crm_configuration_id', $crm->id)
->setParameter('type', Stage::TYPE_OPPORTUNITY)
;
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];
$result[$row['pipeline_id']][] = [
'value' => $value,
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
private function createQueryBuilder(string $realm): QueryBuilder
{
return (new QueryBuilder($this->connection))
->setRealm($realm)
->from('opportunities', 'opp')
->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')
->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')
->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')
;
}
/**
* Applies all applicable visitors and returns the IDs of the executed ones
*
* @return string[]
*/
private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array
{
$queryVisitors = [];
foreach ($this->visitors as $visitor) {
if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {
$visitor->visit($queryBuilder, $criteria);
$queryVisitors[] = $visitor->getIdentifier();
}
}
return $queryVisitors;
}
private function hydrateStages(array $deals): array
{
foreach ($this->fetchStages(array_keys($deals)) as $stage) {
$oppId = (int) $stage['opportunity_id'];
if (! isset($deals[$oppId])) {
continue; // or throw??!
}
$deals[$oppId]['stages'][] = [
'id' => $stage['stage_id'],
'name' => $stage['label'],
'enteredAt' => $stage['created_at'],
];
}
return $deals;
}
/**
* @param int[] $dealIds
*/
private function fetchStages(array $dealIds): array
{
if (empty($dealIds)) {
return [];
}
$qb = new QueryBuilder($this->connection);
$qb
->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')
->from('opportunity_stages', 'os')
->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')
->where($qb->expr()->in('os.opportunity_id', $dealIds))
->orderBy('os.opportunity_id', 'ASC')
->addOrderBy('s.created_at', 'ASC')
;
return $qb->executeQuery()->fetchAllAssociative();
}
private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array
{
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$data = [
'uuid' => RequiresUUID::toNormal($row['uuid']),
'name' => $row['name'],
'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),
'account' => [
'name' => $row['acc_name'],
'url' => $crmService->generateProviderUrl(
providerId: $row['acc_provider_id'],
objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'
),
],
'owner' => null,
'rawValue' => [
'amount' => (float) $row['value'],
'currency' => $row['currency_code'],
],
'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),
'openDate' => $row['remotely_created_at'] ?? null,
'closeDate' => $row['close_date'] ?? null,
'stages' => [],
'currentPipelineId' => $row['pipeline_id'],
'currentStage' => [
'id' => $row['stage_id'],
'enteredAt' => $row['stage_updated_at'],
],
'currentStageUpdatedAt' => $row['stage_updated_at'],
'isClosed' => (bool) $row['is_closed'],
'isWon' => (bool) $row['is_won'],
];
if (isset($row['owner_uuid'])) {
$data['owner'] = [
'uuid' => RequiresUUID::toNormal($row['owner_uuid']),
'name' => $row['owner_name'],
'photoUrl' => $row['owner_photo'] === null
? null
: client_cdn($row['owner_photo'], $team),
'id' => $row['owner_id'],
'job' => $row['owner_job'],
];
}
$result[(int) $row['opp_id']] = $data;
}
return $this->hydrateStages($result);
}
private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder
{
$qb = clone $queryBuilder;
$qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');
$qb
->select(...[
'opp.id as opp_id',
'opp.uuid',
'opp.name',
'opp.value',
'opp.currency_code',
'opp.close_date',
'opp.remotely_created_at',
'opp.is_closed',
'opp.is_won',
])
->addSelect(...[
'usr.uuid as owner_uuid',
'usr.name AS owner_name',
'usr.photo_path as owner_photo',
'usr.id AS owner_id',
'jt.name as owner_job',
])
->addSelect('opp.stage_id', 'opp.stage_updated_at')
->addSelect(...[
'acc.name AS acc_name',
'acc.is_internal as acc_is_internal',
'opp.stage_updated_at',
'acc.crm_provider_id AS acc_provider_id',
'opp.crm_provider_id AS opp_provider_id',
])
->addSelect('rt.business_process_id AS pipeline_id')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'));
return $qb;
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws SocialAccountTokenInvalidException
*/
private function getCrmService(Team $team): ServiceInterface
{
$crmService = $this->providerRegistry->get($team->crm->provider);
$crmService->setConfiguration($team->crm);
if ($crmService instanceof UrlGeneratorInterface) {
$crmService->setCrmUrlGenerator($team->crm);
}
return $crmService;
}
/**
*
* @return Generator<DealData>
*/
public function getForecastData(DealsFilter $filter): Generator
{
$opportunities = DB::query()
->select([
'o.value',
'o.close_date',
'o.currency_code',
'o.is_won',
'o.is_closed',
'o.probability',
'o.forecast_category',
])
->from('opportunities', 'o')
->join('users', 'users.id', '=', 'o.user_id')
->join('groups', 'groups.id', '=', 'users.group_id')
->where('users.team_id', $filter->getTeam()->getId())
->where('o.close_date', '>=', $filter->getStartDate())
->where('o.close_date', '<=', $filter->getEndDate())
->where('o.currency_code', $filter->getCurrency())
->where('o.deleted_at', '=', null)
;
$userUuidList = $filter->getUserUuidList();
if (! empty($userUuidList)) {
$userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);
$opportunities->whereIn('users.uuid', $userUuidList);
}
$groupUuidList = $filter->getGroupUuidList();
if (! empty($groupUuidList)) {
$groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);
$opportunities->whereIn('groups.uuid', $groupUuidList);
}
foreach ($opportunities->cursor() as $row) {
yield new DealData(
(float) $row->value,
$row->close_date,
! empty($row->is_won),
! empty($row->is_closed),
$row->probability ?: 0,
$row->forecast_category ?: '',
);
}
}
public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection
{
return $user->subscriptionSets()
->where(static function (Eloquent\Builder $query): void {
$query
->whereNull('expired_at')
->orWhere('expired_at', '>=', now());
})
->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {
$join
->on('subscription_set_id', '=', 'activity_subscription_sets.id');
$join
->where('followable_type', Models\Activity\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)
->whereIn('followable_id', $opportunityIds);
})
->pluck('followable_id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20157-AJ-report-not-send-notification, menu","depth":5,"help_text":"Git Branch: JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"RequestGenerateAskJiminnyReportJobTest","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"16","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"7","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active Ask Jiminny reports whose expiry date has passed.\n *\n * @return Collection<AutomatedReport>\n */\n public function getExpiredActiveAskJiminnyReports(): Collection\n {\n return AutomatedReport::where('status', true)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereNotNull('expires_at')\n ->where('expires_at', '<', now()->toDateString())\n ->get();\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('report_id', $report->getId())\n ->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])\n ->latest()\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Restrict a query on the automated_reports table to reports the given user is allowed to see.\n *\n * Matches the customer-facing audience:\n * - explicit user recipients (recipients.users)\n * - members of any of the report's groups (Ask Jiminny reports)\n */\n private function applyUserAccessScope(Builder $query, User $user): void\n {\n $userId = $user->getId();\n $groupId = $user->getGroupId();\n\n $query\n ->where('automated_reports.team_id', $user->getTeamId())\n ->where(function (Builder $q) use ($userId, $groupId): void {\n $q->whereJsonContains('automated_reports.recipients->users', $userId);\n\n if ($groupId !== null) {\n $q->orWhere(function (Builder $sub) use ($groupId): void {\n $sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereJsonContains('automated_reports.groups', $groupId);\n });\n }\n });\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active Ask Jiminny reports whose expiry date has passed.\n *\n * @return Collection<AutomatedReport>\n */\n public function getExpiredActiveAskJiminnyReports(): Collection\n {\n return AutomatedReport::where('status', true)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereNotNull('expires_at')\n ->where('expires_at', '<', now()->toDateString())\n ->get();\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('report_id', $report->getId())\n ->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])\n ->latest()\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Restrict a query on the automated_reports table to reports the given user is allowed to see.\n *\n * Matches the customer-facing audience:\n * - explicit user recipients (recipients.users)\n * - members of any of the report's groups (Ask Jiminny reports)\n */\n private function applyUserAccessScope(Builder $query, User $user): void\n {\n $userId = $user->getId();\n $groupId = $user->getGroupId();\n\n $query\n ->where('automated_reports.team_id', $user->getTeamId())\n ->where(function (Builder $q) use ($userId, $groupId): void {\n $q->whereJsonContains('automated_reports.recipients->users', $userId);\n\n if ($groupId !== null) {\n $q->orWhere(function (Builder $sub) use ($groupId): void {\n $sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereJsonContains('automated_reports.groups', $groupId);\n });\n }\n });\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"36","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\DealInsights;\n\nuse Doctrine\\DBAL\\Connection;\nuse Generator;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealData;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealsFilter;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\QueryBuilder;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\Visitor\\QueryBuilderVisitorInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Services\\Crm\\IntegrationApp\\DTO\\Utils\\UrlGeneratorInterface;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Eloquent;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass DealsRepository implements DealsRepositoryInterface\n{\n private Connection $connection;\n\n private ProviderRegistry $providerRegistry;\n\n /**\n * @var QueryBuilderVisitorInterface[]\n */\n private array $visitors = [];\n\n /**\n * @param QueryBuilderVisitorInterface[] $visitors\n */\n public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])\n {\n $this->connection = $connection;\n $this->providerRegistry = $crmProviderRegistry;\n\n foreach ($visitors as $visitor) {\n $this->visitors[$visitor->getIdentifier()] = $visitor;\n }\n }\n\n public function getDeals(CriteriaInterface $criteria): array\n {\n $context = $criteria->getContext();\n $team = $context->getTeam();\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n\n $this->visit($qb, $criteria);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getDeal(Team $team, int $id): array\n {\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n $qb->andWhere('opp.id = :id')->setParameter('id', $id);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')\n ->from('crm_fields', 'f')\n ->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')\n ->where('f.crm_configuration_id = :crm')\n ->andWhere('f.object_type = :type')\n ->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')\n ->orderBy('fd.object_id', 'ASC')\n ->addOrderBy('fd.updated_at', 'ASC')\n\n ->setParameter('type', Field::OBJECT_OPPORTUNITY)\n ->setParameter('crm', $crmId)\n ;\n\n if (! empty($crmFields)) {\n $fields = array_map(fn ($value): string => '\"' . $value . '\"', $crmFields);\n $qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');\n }\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAssociative();\n }\n\n public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('COALESCE(opp.currency_code, \"' . $defaultCurrency . '\") AS currency')\n ->addSelect('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ->groupBy('currency')\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getDealActivities(CriteriaInterface $criteria): array\n {\n $qb = Activity::with(['participants', 'user'])\n ->where('opportunity_id', $criteria->getOpportunityId())\n ->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())\n ->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())\n ->orderBy($criteria->getSortBy(), $criteria->getSortDirection())\n ;\n\n // Should we filter activities by criteria? It's intended to filter deals.\n\n return $qb->get()->all();\n }\n\n public function getStages(CriteriaInterface $criteria): array\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('id', 'label', 'sequence')\n ->from('stages', 's')\n ->where('crm_configuration_id = :crm_configuration_id')\n ->andWhere('type = :type')\n ->orderBy('sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())\n ->setParameter('type', Stage::TYPE_OPPORTUNITY);\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $result[$row['id']] = [\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n public function getConfigurationStages(Configuration $configuration): Collection\n {\n return $configuration\n ->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get();\n }\n\n public function getPipelineData(Configuration $crm): array\n {\n $qb = new QueryBuilder($this->connection);\n $provider = $crm->provider;\n\n $qb\n ->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')\n ->from('stages', 's')\n ->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')\n ->where('s.crm_configuration_id = :crm_configuration_id')\n ->andWhere('s.type = :type')\n ->orderBy('bps.business_process_id', 'ASC')\n ->addOrderBy('s.sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $crm->id)\n ->setParameter('type', Stage::TYPE_OPPORTUNITY)\n ;\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];\n $result[$row['pipeline_id']][] = [\n 'value' => $value,\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n private function createQueryBuilder(string $realm): QueryBuilder\n {\n return (new QueryBuilder($this->connection))\n ->setRealm($realm)\n ->from('opportunities', 'opp')\n ->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')\n ->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')\n ->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')\n ;\n }\n\n /**\n * Applies all applicable visitors and returns the IDs of the executed ones\n *\n * @return string[]\n */\n private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array\n {\n $queryVisitors = [];\n\n foreach ($this->visitors as $visitor) {\n if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {\n $visitor->visit($queryBuilder, $criteria);\n\n $queryVisitors[] = $visitor->getIdentifier();\n }\n }\n\n return $queryVisitors;\n }\n\n private function hydrateStages(array $deals): array\n {\n foreach ($this->fetchStages(array_keys($deals)) as $stage) {\n $oppId = (int) $stage['opportunity_id'];\n\n if (! isset($deals[$oppId])) {\n continue; // or throw??!\n }\n\n $deals[$oppId]['stages'][] = [\n 'id' => $stage['stage_id'],\n 'name' => $stage['label'],\n 'enteredAt' => $stage['created_at'],\n ];\n }\n\n return $deals;\n }\n\n /**\n * @param int[] $dealIds\n */\n private function fetchStages(array $dealIds): array\n {\n if (empty($dealIds)) {\n return [];\n }\n\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')\n ->from('opportunity_stages', 'os')\n ->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')\n ->where($qb->expr()->in('os.opportunity_id', $dealIds))\n ->orderBy('os.opportunity_id', 'ASC')\n ->addOrderBy('s.created_at', 'ASC')\n ;\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array\n {\n $result = [];\n\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $data = [\n 'uuid' => RequiresUUID::toNormal($row['uuid']),\n 'name' => $row['name'],\n 'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),\n 'account' => [\n 'name' => $row['acc_name'],\n 'url' => $crmService->generateProviderUrl(\n providerId: $row['acc_provider_id'],\n objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'\n ),\n ],\n 'owner' => null,\n 'rawValue' => [\n 'amount' => (float) $row['value'],\n 'currency' => $row['currency_code'],\n ],\n 'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),\n 'openDate' => $row['remotely_created_at'] ?? null,\n 'closeDate' => $row['close_date'] ?? null,\n 'stages' => [],\n 'currentPipelineId' => $row['pipeline_id'],\n 'currentStage' => [\n 'id' => $row['stage_id'],\n 'enteredAt' => $row['stage_updated_at'],\n ],\n 'currentStageUpdatedAt' => $row['stage_updated_at'],\n 'isClosed' => (bool) $row['is_closed'],\n 'isWon' => (bool) $row['is_won'],\n ];\n\n if (isset($row['owner_uuid'])) {\n $data['owner'] = [\n 'uuid' => RequiresUUID::toNormal($row['owner_uuid']),\n 'name' => $row['owner_name'],\n 'photoUrl' => $row['owner_photo'] === null\n ? null\n : client_cdn($row['owner_photo'], $team),\n 'id' => $row['owner_id'],\n 'job' => $row['owner_job'],\n ];\n }\n\n $result[(int) $row['opp_id']] = $data;\n }\n\n return $this->hydrateStages($result);\n }\n\n private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder\n {\n $qb = clone $queryBuilder;\n $qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');\n\n $qb\n ->select(...[\n 'opp.id as opp_id',\n 'opp.uuid',\n 'opp.name',\n 'opp.value',\n 'opp.currency_code',\n 'opp.close_date',\n 'opp.remotely_created_at',\n 'opp.is_closed',\n 'opp.is_won',\n ])\n ->addSelect(...[\n 'usr.uuid as owner_uuid',\n 'usr.name AS owner_name',\n 'usr.photo_path as owner_photo',\n 'usr.id AS owner_id',\n 'jt.name as owner_job',\n ])\n ->addSelect('opp.stage_id', 'opp.stage_updated_at')\n ->addSelect(...[\n 'acc.name AS acc_name',\n 'acc.is_internal as acc_is_internal',\n 'opp.stage_updated_at',\n 'acc.crm_provider_id AS acc_provider_id',\n 'opp.crm_provider_id AS opp_provider_id',\n ])\n ->addSelect('rt.business_process_id AS pipeline_id')\n\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'));\n\n return $qb;\n }\n\n /**\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws SocialAccountTokenInvalidException\n */\n private function getCrmService(Team $team): ServiceInterface\n {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n $crmService->setConfiguration($team->crm);\n if ($crmService instanceof UrlGeneratorInterface) {\n $crmService->setCrmUrlGenerator($team->crm);\n }\n\n return $crmService;\n }\n\n /**\n *\n * @return Generator<DealData>\n */\n public function getForecastData(DealsFilter $filter): Generator\n {\n $opportunities = DB::query()\n ->select([\n 'o.value',\n 'o.close_date',\n 'o.currency_code',\n 'o.is_won',\n 'o.is_closed',\n 'o.probability',\n 'o.forecast_category',\n ])\n ->from('opportunities', 'o')\n ->join('users', 'users.id', '=', 'o.user_id')\n ->join('groups', 'groups.id', '=', 'users.group_id')\n ->where('users.team_id', $filter->getTeam()->getId())\n ->where('o.close_date', '>=', $filter->getStartDate())\n ->where('o.close_date', '<=', $filter->getEndDate())\n ->where('o.currency_code', $filter->getCurrency())\n ->where('o.deleted_at', '=', null)\n ;\n\n $userUuidList = $filter->getUserUuidList();\n if (! empty($userUuidList)) {\n $userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);\n\n $opportunities->whereIn('users.uuid', $userUuidList);\n }\n\n $groupUuidList = $filter->getGroupUuidList();\n if (! empty($groupUuidList)) {\n $groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);\n\n $opportunities->whereIn('groups.uuid', $groupUuidList);\n }\n\n foreach ($opportunities->cursor() as $row) {\n yield new DealData(\n (float) $row->value,\n $row->close_date,\n ! empty($row->is_won),\n ! empty($row->is_closed),\n $row->probability ?: 0,\n $row->forecast_category ?: '',\n );\n }\n }\n\n public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection\n {\n return $user->subscriptionSets()\n ->where(static function (Eloquent\\Builder $query): void {\n $query\n ->whereNull('expired_at')\n ->orWhere('expired_at', '>=', now());\n })\n ->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n $join\n ->where('followable_type', Models\\Activity\\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)\n ->whereIn('followable_id', $opportunityIds);\n })\n ->pluck('followable_id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\DealInsights;\n\nuse Doctrine\\DBAL\\Connection;\nuse Generator;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealData;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealsFilter;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\QueryBuilder;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\Visitor\\QueryBuilderVisitorInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Services\\Crm\\IntegrationApp\\DTO\\Utils\\UrlGeneratorInterface;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Eloquent;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass DealsRepository implements DealsRepositoryInterface\n{\n private Connection $connection;\n\n private ProviderRegistry $providerRegistry;\n\n /**\n * @var QueryBuilderVisitorInterface[]\n */\n private array $visitors = [];\n\n /**\n * @param QueryBuilderVisitorInterface[] $visitors\n */\n public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])\n {\n $this->connection = $connection;\n $this->providerRegistry = $crmProviderRegistry;\n\n foreach ($visitors as $visitor) {\n $this->visitors[$visitor->getIdentifier()] = $visitor;\n }\n }\n\n public function getDeals(CriteriaInterface $criteria): array\n {\n $context = $criteria->getContext();\n $team = $context->getTeam();\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n\n $this->visit($qb, $criteria);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getDeal(Team $team, int $id): array\n {\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n $qb->andWhere('opp.id = :id')->setParameter('id', $id);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')\n ->from('crm_fields', 'f')\n ->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')\n ->where('f.crm_configuration_id = :crm')\n ->andWhere('f.object_type = :type')\n ->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')\n ->orderBy('fd.object_id', 'ASC')\n ->addOrderBy('fd.updated_at', 'ASC')\n\n ->setParameter('type', Field::OBJECT_OPPORTUNITY)\n ->setParameter('crm', $crmId)\n ;\n\n if (! empty($crmFields)) {\n $fields = array_map(fn ($value): string => '\"' . $value . '\"', $crmFields);\n $qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');\n }\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAssociative();\n }\n\n public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('COALESCE(opp.currency_code, \"' . $defaultCurrency . '\") AS currency')\n ->addSelect('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ->groupBy('currency')\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getDealActivities(CriteriaInterface $criteria): array\n {\n $qb = Activity::with(['participants', 'user'])\n ->where('opportunity_id', $criteria->getOpportunityId())\n ->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())\n ->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())\n ->orderBy($criteria->getSortBy(), $criteria->getSortDirection())\n ;\n\n // Should we filter activities by criteria? It's intended to filter deals.\n\n return $qb->get()->all();\n }\n\n public function getStages(CriteriaInterface $criteria): array\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('id', 'label', 'sequence')\n ->from('stages', 's')\n ->where('crm_configuration_id = :crm_configuration_id')\n ->andWhere('type = :type')\n ->orderBy('sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())\n ->setParameter('type', Stage::TYPE_OPPORTUNITY);\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $result[$row['id']] = [\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n public function getConfigurationStages(Configuration $configuration): Collection\n {\n return $configuration\n ->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get();\n }\n\n public function getPipelineData(Configuration $crm): array\n {\n $qb = new QueryBuilder($this->connection);\n $provider = $crm->provider;\n\n $qb\n ->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')\n ->from('stages', 's')\n ->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')\n ->where('s.crm_configuration_id = :crm_configuration_id')\n ->andWhere('s.type = :type')\n ->orderBy('bps.business_process_id', 'ASC')\n ->addOrderBy('s.sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $crm->id)\n ->setParameter('type', Stage::TYPE_OPPORTUNITY)\n ;\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];\n $result[$row['pipeline_id']][] = [\n 'value' => $value,\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n private function createQueryBuilder(string $realm): QueryBuilder\n {\n return (new QueryBuilder($this->connection))\n ->setRealm($realm)\n ->from('opportunities', 'opp')\n ->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')\n ->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')\n ->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')\n ;\n }\n\n /**\n * Applies all applicable visitors and returns the IDs of the executed ones\n *\n * @return string[]\n */\n private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array\n {\n $queryVisitors = [];\n\n foreach ($this->visitors as $visitor) {\n if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {\n $visitor->visit($queryBuilder, $criteria);\n\n $queryVisitors[] = $visitor->getIdentifier();\n }\n }\n\n return $queryVisitors;\n }\n\n private function hydrateStages(array $deals): array\n {\n foreach ($this->fetchStages(array_keys($deals)) as $stage) {\n $oppId = (int) $stage['opportunity_id'];\n\n if (! isset($deals[$oppId])) {\n continue; // or throw??!\n }\n\n $deals[$oppId]['stages'][] = [\n 'id' => $stage['stage_id'],\n 'name' => $stage['label'],\n 'enteredAt' => $stage['created_at'],\n ];\n }\n\n return $deals;\n }\n\n /**\n * @param int[] $dealIds\n */\n private function fetchStages(array $dealIds): array\n {\n if (empty($dealIds)) {\n return [];\n }\n\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')\n ->from('opportunity_stages', 'os')\n ->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')\n ->where($qb->expr()->in('os.opportunity_id', $dealIds))\n ->orderBy('os.opportunity_id', 'ASC')\n ->addOrderBy('s.created_at', 'ASC')\n ;\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array\n {\n $result = [];\n\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $data = [\n 'uuid' => RequiresUUID::toNormal($row['uuid']),\n 'name' => $row['name'],\n 'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),\n 'account' => [\n 'name' => $row['acc_name'],\n 'url' => $crmService->generateProviderUrl(\n providerId: $row['acc_provider_id'],\n objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'\n ),\n ],\n 'owner' => null,\n 'rawValue' => [\n 'amount' => (float) $row['value'],\n 'currency' => $row['currency_code'],\n ],\n 'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),\n 'openDate' => $row['remotely_created_at'] ?? null,\n 'closeDate' => $row['close_date'] ?? null,\n 'stages' => [],\n 'currentPipelineId' => $row['pipeline_id'],\n 'currentStage' => [\n 'id' => $row['stage_id'],\n 'enteredAt' => $row['stage_updated_at'],\n ],\n 'currentStageUpdatedAt' => $row['stage_updated_at'],\n 'isClosed' => (bool) $row['is_closed'],\n 'isWon' => (bool) $row['is_won'],\n ];\n\n if (isset($row['owner_uuid'])) {\n $data['owner'] = [\n 'uuid' => RequiresUUID::toNormal($row['owner_uuid']),\n 'name' => $row['owner_name'],\n 'photoUrl' => $row['owner_photo'] === null\n ? null\n : client_cdn($row['owner_photo'], $team),\n 'id' => $row['owner_id'],\n 'job' => $row['owner_job'],\n ];\n }\n\n $result[(int) $row['opp_id']] = $data;\n }\n\n return $this->hydrateStages($result);\n }\n\n private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder\n {\n $qb = clone $queryBuilder;\n $qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');\n\n $qb\n ->select(...[\n 'opp.id as opp_id',\n 'opp.uuid',\n 'opp.name',\n 'opp.value',\n 'opp.currency_code',\n 'opp.close_date',\n 'opp.remotely_created_at',\n 'opp.is_closed',\n 'opp.is_won',\n ])\n ->addSelect(...[\n 'usr.uuid as owner_uuid',\n 'usr.name AS owner_name',\n 'usr.photo_path as owner_photo',\n 'usr.id AS owner_id',\n 'jt.name as owner_job',\n ])\n ->addSelect('opp.stage_id', 'opp.stage_updated_at')\n ->addSelect(...[\n 'acc.name AS acc_name',\n 'acc.is_internal as acc_is_internal',\n 'opp.stage_updated_at',\n 'acc.crm_provider_id AS acc_provider_id',\n 'opp.crm_provider_id AS opp_provider_id',\n ])\n ->addSelect('rt.business_process_id AS pipeline_id')\n\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'));\n\n return $qb;\n }\n\n /**\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws SocialAccountTokenInvalidException\n */\n private function getCrmService(Team $team): ServiceInterface\n {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n $crmService->setConfiguration($team->crm);\n if ($crmService instanceof UrlGeneratorInterface) {\n $crmService->setCrmUrlGenerator($team->crm);\n }\n\n return $crmService;\n }\n\n /**\n *\n * @return Generator<DealData>\n */\n public function getForecastData(DealsFilter $filter): Generator\n {\n $opportunities = DB::query()\n ->select([\n 'o.value',\n 'o.close_date',\n 'o.currency_code',\n 'o.is_won',\n 'o.is_closed',\n 'o.probability',\n 'o.forecast_category',\n ])\n ->from('opportunities', 'o')\n ->join('users', 'users.id', '=', 'o.user_id')\n ->join('groups', 'groups.id', '=', 'users.group_id')\n ->where('users.team_id', $filter->getTeam()->getId())\n ->where('o.close_date', '>=', $filter->getStartDate())\n ->where('o.close_date', '<=', $filter->getEndDate())\n ->where('o.currency_code', $filter->getCurrency())\n ->where('o.deleted_at', '=', null)\n ;\n\n $userUuidList = $filter->getUserUuidList();\n if (! empty($userUuidList)) {\n $userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);\n\n $opportunities->whereIn('users.uuid', $userUuidList);\n }\n\n $groupUuidList = $filter->getGroupUuidList();\n if (! empty($groupUuidList)) {\n $groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);\n\n $opportunities->whereIn('groups.uuid', $groupUuidList);\n }\n\n foreach ($opportunities->cursor() as $row) {\n yield new DealData(\n (float) $row->value,\n $row->close_date,\n ! empty($row->is_won),\n ! empty($row->is_closed),\n $row->probability ?: 0,\n $row->forecast_category ?: '',\n );\n }\n }\n\n public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection\n {\n return $user->subscriptionSets()\n ->where(static function (Eloquent\\Builder $query): void {\n $query\n ->whereNull('expired_at')\n ->orWhere('expired_at', '>=', now());\n })\n ->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n $join\n ->where('followable_type', Models\\Activity\\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)\n ->whereIn('followable_id', $opportunityIds);\n })\n ->pluck('followable_id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3407362332434637772
|
-4167466460282672524
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
16
7
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active Ask Jiminny reports whose expiry date has passed.
*
* @return Collection<AutomatedReport>
*/
public function getExpiredActiveAskJiminnyReports(): Collection
{
return AutomatedReport::where('status', true)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereNotNull('expires_at')
->where('expires_at', '<', now()->toDateString())
->get();
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('report_id', $report->getId())
->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])
->latest()
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Restrict a query on the automated_reports table to reports the given user is allowed to see.
*
* Matches the customer-facing audience:
* - explicit user recipients (recipients.users)
* - members of any of the report's groups (Ask Jiminny reports)
*/
private function applyUserAccessScope(Builder $query, User $user): void
{
$userId = $user->getId();
$groupId = $user->getGroupId();
$query
->where('automated_reports.team_id', $user->getTeamId())
->where(function (Builder $q) use ($userId, $groupId): void {
$q->whereJsonContains('automated_reports.recipients->users', $userId);
if ($groupId !== null) {
$q->orWhere(function (Builder $sub) use ($groupId): void {
$sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereJsonContains('automated_reports.groups', $groupId);
});
}
});
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
36
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\DealInsights;
use Doctrine\DBAL\Connection;
use Generator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Jiminny\Component\DealInsights\Forecast\DealData;
use Jiminny\Component\DealInsights\Forecast\DealsFilter;
use Jiminny\Component\DealInsights\QueryBuilder\QueryBuilder;
use Jiminny\Component\DealInsights\QueryBuilder\Visitor\QueryBuilderVisitorInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Services\Crm\IntegrationApp\DTO\Utils\UrlGeneratorInterface;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class DealsRepository implements DealsRepositoryInterface
{
private Connection $connection;
private ProviderRegistry $providerRegistry;
/**
* @var QueryBuilderVisitorInterface[]
*/
private array $visitors = [];
/**
* @param QueryBuilderVisitorInterface[] $visitors
*/
public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])
{
$this->connection = $connection;
$this->providerRegistry = $crmProviderRegistry;
foreach ($visitors as $visitor) {
$this->visitors[$visitor->getIdentifier()] = $visitor;
}
}
public function getDeals(CriteriaInterface $criteria): array
{
$context = $criteria->getContext();
$team = $context->getTeam();
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$this->visit($qb, $criteria);
return $this->execute($team, $crmService, $qb);
}
public function getDeal(Team $team, int $id): array
{
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$qb->andWhere('opp.id = :id')->setParameter('id', $id);
return $this->execute($team, $crmService, $qb);
}
public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])
{
$qb = new QueryBuilder($this->connection);
$qb
->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')
->from('crm_fields', 'f')
->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')
->where('f.crm_configuration_id = :crm')
->andWhere('f.object_type = :type')
->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')
->orderBy('fd.object_id', 'ASC')
->addOrderBy('fd.updated_at', 'ASC')
->setParameter('type', Field::OBJECT_OPPORTUNITY)
->setParameter('crm', $crmId)
;
if (! empty($crmFields)) {
$fields = array_map(fn ($value): string => '"' . $value . '"', $crmFields);
$qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');
}
return $qb->executeQuery()->fetchAllAssociative();
}
public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAssociative();
}
public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('COALESCE(opp.currency_code, "' . $defaultCurrency . '") AS currency')
->addSelect('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
->groupBy('currency')
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAllAssociative();
}
public function getDealActivities(CriteriaInterface $criteria): array
{
$qb = Activity::with(['participants', 'user'])
->where('opportunity_id', $criteria->getOpportunityId())
->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())
->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())
->orderBy($criteria->getSortBy(), $criteria->getSortDirection())
;
// Should we filter activities by criteria? It's intended to filter deals.
return $qb->get()->all();
}
public function getStages(CriteriaInterface $criteria): array
{
$qb = new QueryBuilder($this->connection);
$qb
->select('id', 'label', 'sequence')
->from('stages', 's')
->where('crm_configuration_id = :crm_configuration_id')
->andWhere('type = :type')
->orderBy('sequence', 'ASC')
->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())
->setParameter('type', Stage::TYPE_OPPORTUNITY);
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$result[$row['id']] = [
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
public function getConfigurationStages(Configuration $configuration): Collection
{
return $configuration
->stages()
->where('type', Stage::TYPE_OPPORTUNITY)
->get();
}
public function getPipelineData(Configuration $crm): array
{
$qb = new QueryBuilder($this->connection);
$provider = $crm->provider;
$qb
->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')
->from('stages', 's')
->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')
->where('s.crm_configuration_id = :crm_configuration_id')
->andWhere('s.type = :type')
->orderBy('bps.business_process_id', 'ASC')
->addOrderBy('s.sequence', 'ASC')
->setParameter('crm_configuration_id', $crm->id)
->setParameter('type', Stage::TYPE_OPPORTUNITY)
;
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];
$result[$row['pipeline_id']][] = [
'value' => $value,
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
private function createQueryBuilder(string $realm): QueryBuilder
{
return (new QueryBuilder($this->connection))
->setRealm($realm)
->from('opportunities', 'opp')
->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')
->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')
->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')
;
}
/**
* Applies all applicable visitors and returns the IDs of the executed ones
*
* @return string[]
*/
private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array
{
$queryVisitors = [];
foreach ($this->visitors as $visitor) {
if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {
$visitor->visit($queryBuilder, $criteria);
$queryVisitors[] = $visitor->getIdentifier();
}
}
return $queryVisitors;
}
private function hydrateStages(array $deals): array
{
foreach ($this->fetchStages(array_keys($deals)) as $stage) {
$oppId = (int) $stage['opportunity_id'];
if (! isset($deals[$oppId])) {
continue; // or throw??!
}
$deals[$oppId]['stages'][] = [
'id' => $stage['stage_id'],
'name' => $stage['label'],
'enteredAt' => $stage['created_at'],
];
}
return $deals;
}
/**
* @param int[] $dealIds
*/
private function fetchStages(array $dealIds): array
{
if (empty($dealIds)) {
return [];
}
$qb = new QueryBuilder($this->connection);
$qb
->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')
->from('opportunity_stages', 'os')
->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')
->where($qb->expr()->in('os.opportunity_id', $dealIds))
->orderBy('os.opportunity_id', 'ASC')
->addOrderBy('s.created_at', 'ASC')
;
return $qb->executeQuery()->fetchAllAssociative();
}
private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array
{
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$data = [
'uuid' => RequiresUUID::toNormal($row['uuid']),
'name' => $row['name'],
'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),
'account' => [
'name' => $row['acc_name'],
'url' => $crmService->generateProviderUrl(
providerId: $row['acc_provider_id'],
objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'
),
],
'owner' => null,
'rawValue' => [
'amount' => (float) $row['value'],
'currency' => $row['currency_code'],
],
'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),
'openDate' => $row['remotely_created_at'] ?? null,
'closeDate' => $row['close_date'] ?? null,
'stages' => [],
'currentPipelineId' => $row['pipeline_id'],
'currentStage' => [
'id' => $row['stage_id'],
'enteredAt' => $row['stage_updated_at'],
],
'currentStageUpdatedAt' => $row['stage_updated_at'],
'isClosed' => (bool) $row['is_closed'],
'isWon' => (bool) $row['is_won'],
];
if (isset($row['owner_uuid'])) {
$data['owner'] = [
'uuid' => RequiresUUID::toNormal($row['owner_uuid']),
'name' => $row['owner_name'],
'photoUrl' => $row['owner_photo'] === null
? null
: client_cdn($row['owner_photo'], $team),
'id' => $row['owner_id'],
'job' => $row['owner_job'],
];
}
$result[(int) $row['opp_id']] = $data;
}
return $this->hydrateStages($result);
}
private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder
{
$qb = clone $queryBuilder;
$qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');
$qb
->select(...[
'opp.id as opp_id',
'opp.uuid',
'opp.name',
'opp.value',
'opp.currency_code',
'opp.close_date',
'opp.remotely_created_at',
'opp.is_closed',
'opp.is_won',
])
->addSelect(...[
'usr.uuid as owner_uuid',
'usr.name AS owner_name',
'usr.photo_path as owner_photo',
'usr.id AS owner_id',
'jt.name as owner_job',
])
->addSelect('opp.stage_id', 'opp.stage_updated_at')
->addSelect(...[
'acc.name AS acc_name',
'acc.is_internal as acc_is_internal',
'opp.stage_updated_at',
'acc.crm_provider_id AS acc_provider_id',
'opp.crm_provider_id AS opp_provider_id',
])
->addSelect('rt.business_process_id AS pipeline_id')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'));
return $qb;
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws SocialAccountTokenInvalidException
*/
private function getCrmService(Team $team): ServiceInterface
{
$crmService = $this->providerRegistry->get($team->crm->provider);
$crmService->setConfiguration($team->crm);
if ($crmService instanceof UrlGeneratorInterface) {
$crmService->setCrmUrlGenerator($team->crm);
}
return $crmService;
}
/**
*
* @return Generator<DealData>
*/
public function getForecastData(DealsFilter $filter): Generator
{
$opportunities = DB::query()
->select([
'o.value',
'o.close_date',
'o.currency_code',
'o.is_won',
'o.is_closed',
'o.probability',
'o.forecast_category',
])
->from('opportunities', 'o')
->join('users', 'users.id', '=', 'o.user_id')
->join('groups', 'groups.id', '=', 'users.group_id')
->where('users.team_id', $filter->getTeam()->getId())
->where('o.close_date', '>=', $filter->getStartDate())
->where('o.close_date', '<=', $filter->getEndDate())
->where('o.currency_code', $filter->getCurrency())
->where('o.deleted_at', '=', null)
;
$userUuidList = $filter->getUserUuidList();
if (! empty($userUuidList)) {
$userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);
$opportunities->whereIn('users.uuid', $userUuidList);
}
$groupUuidList = $filter->getGroupUuidList();
if (! empty($groupUuidList)) {
$groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);
$opportunities->whereIn('groups.uuid', $groupUuidList);
}
foreach ($opportunities->cursor() as $row) {
yield new DealData(
(float) $row->value,
$row->close_date,
! empty($row->is_won),
! empty($row->is_closed),
$row->probability ?: 0,
$row->forecast_category ?: '',
);
}
}
public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection
{
return $user->subscriptionSets()
->where(static function (Eloquent\Builder $query): void {
$query
->whereNull('expired_at')
->orWhere('expired_at', '>=', now());
})
->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {
$join
->on('subscription_set_id', '=', 'activity_subscription_sets.id');
$join
->where('followable_type', Models\Activity\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)
->whereIn('followable_id', $opportunityIds);
})
->pluck('followable_id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
73164
|
|
73130
|
NULL
|
0
|
2026-04-23T06:50:15.395333+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776927015395_m1.jpg...
|
Firefox
|
Sign in - Google Accounts — Work
|
True
|
accounts.google.com/v3/signin/accountchooser?acces accounts.google.com/v3/signin/accountchooser?access_type=offline&client_id=1092434469059-baf640lpublohrbvt5kbv356gstujeha.apps.googleusercontent.com&include_granted_scopes=true&redirect_uri=https%3A%2F%2Furanus.staging.jiminny.com%2Fauth%2Fcallback%2Fgoogle&response_type=code&scope=openid+email+profile&state=eyJyYW5kb20iOiJkMWI5Mzk4M2JlZDY1OTI3NWMzYzE3YWQ2MjJkN2NmOSIsInRpbWUiOjE3NzY5MjcwMTUuNTk3OTU1LCJyZWRpcmVjdCI6IlwvbG9naW4iLCJpc19sb2dpbiI6dHJ1ZSwicmVkaXJlY3RVcmwiOiIifQ--&dsh=S-1155073835%3A1776927016679402&o2v=2&service=lso&flowName=GeneralOAuthFlow&opparams=%253F&continue=https%3A%2F%2Faccounts.google.com%2Fsignin%2Foauth%2Fconsent%3Fauthuser%3Dunknown%26part%3DAJi8hAOW-ID6WBJArs2Jdy_5-Yc7DIikeZZG3WcQc8IohAzb0qf14LjmuGza3fUcIu8a_LD9QppTjrBrXV0vjVYuJRIMh0xNPYK_w9f3asLQRd19GuY64qeva3ti4d9knTRIsnkAF-4_6IX0SCFgVTYiXNOWC1EVhLScoD5o9p_yCE4xzG1EXGwcF4jBSJjdxYbnhzdv4IsdQn2IT167uOlfHi35mZdK_9mhiN3UoyrDiQ8Zalliib4UNSlO9ORgV5nXLdjLb4TZSejCfwLq6q6uFvBCvF0BLFv0jH-BujMbM-1M0tE30Y96vCb6ja3e4THBtEr8tLG_EwBx9c8yClWK3NfQ_M9Hh1iFqN9I6cDYjR8-MJwytyT-qGsp5DWDBrIgWrurdCn6BELTHvVETGw2xzUqKwxLANRnlhtAUWWccG2Ls-6jCTwZvcNEUA7yvyGEbW7WUwrtCNAz67Viwj0M05OJJ_lYq3g1NnlVxFXlRIsWcz8s7tU%26flowName%3DGeneralOAuthFlow%26as%3DS-1155073835%253A1776927016679402%26client_id%3D1092434469059-baf640lpublohrbvt5kbv356gstujeha.apps.googleusercontent.com%26requestPath%3D%252Fsignin%252Foauth%252Fconsent%23&app_domain=https%3A%2F%2Furanus.staging.jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
New Tab
New Tab
AI reports promotion pages by nikolay-yankov · Pull Request #11998 · jiminny/app
AI reports promotion pages by nikolay-yankov · Pull Request #11998 · jiminny/app
Pull requests · jiminny/app
Pull requests · jiminny/app
Sign in - Google Accounts
Sign in - Google Accounts
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Sign in with Google
Choose an account
Choose an account
to continue to
jiminny.com
[EMAIL]
[EMAIL]
Integration Account [EMAIL]
Integration Account
[EMAIL]
Use another account
Use another account
Before using this app, you can review jiminny.com’s
Privacy Policy
Privacy Policy
and
Terms of Service
Terms of Service
.
English (United States)
English (United States)
Help
Help
Privacy
Privacy
Terms
Terms
Sending request to play.google.com…...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"New Tab","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"AI reports promotion pages by nikolay-yankov · Pull Request #11998 · jiminny/app","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"AI reports promotion pages by nikolay-yankov · Pull Request #11998 · jiminny/app","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pull requests · jiminny/app","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · jiminny/app","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Sign in - Google Accounts","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Sign in - Google Accounts","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0,"top":0.0,"width":0.022222223,"height":0.035555556},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.0,"top":0.0,"width":0.022222223,"height":0.035555556},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.0,"top":0.0,"width":0.022222223,"height":0.035555556},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.0,"top":0.0,"width":0.022222223,"height":0.035555556},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.017361112,"top":0.0,"width":0.022222223,"height":0.035555556},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Sign in with Google","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Choose an account","depth":10,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Choose an account","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to continue to","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"jiminny.com","depth":11,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"lukas.kovalik@jiminny.com","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"lukas.kovalik@jiminny.com","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Integration Account integration-account@jiminny.com","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Integration Account","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"integration-account@jiminny.com","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Use another account","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Use another account","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Before using this app, you can review jiminny.com’s","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Privacy Policy","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Privacy Policy","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Terms of Service","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Terms of Service","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXComboBox","text":"English (United States)","depth":10,"value":"English (United States)","help_text":"","role_description":"combo box","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"English (United States)","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Help","depth":11,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Help","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Privacy","depth":11,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Privacy","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Terms","depth":11,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Terms","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Sending request to play.google.com…","depth":5,"bounds":{"left":0.087847225,"top":0.0,"width":0.13645834,"height":0.015},"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
6915617101784922837
|
-8254417621046894338
|
click
|
accessibility
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
New Tab
New Tab
AI reports promotion pages by nikolay-yankov · Pull Request #11998 · jiminny/app
AI reports promotion pages by nikolay-yankov · Pull Request #11998 · jiminny/app
Pull requests · jiminny/app
Pull requests · jiminny/app
Sign in - Google Accounts
Sign in - Google Accounts
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Sign in with Google
Choose an account
Choose an account
to continue to
jiminny.com
[EMAIL]
[EMAIL]
Integration Account [EMAIL]
Integration Account
[EMAIL]
Use another account
Use another account
Before using this app, you can review jiminny.com’s
Privacy Policy
Privacy Policy
and
Terms of Service
Terms of Service
.
English (United States)
English (United States)
Help
Help
Privacy
Privacy
Terms
Terms
Sending request to play.google.com…...
|
73128
|
|
73129
|
NULL
|
0
|
2026-04-23T06:50:13.089336+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776927013089_m2.jpg...
|
Firefox
|
Jiminny — Work
|
True
|
uranus.staging.jiminny.com/login
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
New Tab
New Tab
AI reports promotion pages by nikolay-yankov · Pull Request #11998 · jiminny/app
AI reports promotion pages by nikolay-yankov · Pull Request #11998 · jiminny/app
Pull requests · jiminny/app
Pull requests · jiminny/app
Jiminny
Jiminny
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Sign In
Sign In
google Sign in with Google
Sign in with Google
microsoft Sign in with Office 365
Sign in with Office 365
Login via SSO
Login via SSO
EU
EU
Worldwide
JY-9712-change-forever-nudges-to-1-year-expiration ■ 874632
Don’t have account yet? Speak to your sales leader to get an invite.
By using this service you agree to the
Privacy Policy
Privacy Policy
and
Terms of Service
Terms of Service
.
TERMS
TERMS
KNOWLEDGE BASE
KNOWLEDGE BASE
BLOG
BLOG
Open Intercom Messenger
Transferring data from uranus.staging.jiminny.com…...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira","depth":4,"bounds":{"left":0.23321144,"top":0.0518755,"width":0.07596409,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"New Tab","depth":4,"bounds":{"left":0.23138298,"top":0.09497207,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"bounds":{"left":0.24468085,"top":0.10614525,"width":0.014960106,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"AI reports promotion pages by nikolay-yankov · Pull Request #11998 · jiminny/app","depth":4,"bounds":{"left":0.23138298,"top":0.12769353,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"AI reports promotion pages by nikolay-yankov · Pull Request #11998 · jiminny/app","depth":5,"bounds":{"left":0.24468085,"top":0.13886672,"width":0.14128989,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pull requests · jiminny/app","depth":4,"bounds":{"left":0.23138298,"top":0.16041501,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · jiminny/app","depth":5,"bounds":{"left":0.24468085,"top":0.17158818,"width":0.04537899,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny","depth":4,"bounds":{"left":0.23138298,"top":0.19313647,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Jiminny","depth":5,"bounds":{"left":0.24468085,"top":0.20430966,"width":0.013131649,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.29870346,"top":0.20031923,"width":0.007978723,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.23420878,"top":0.22745411,"width":0.07413564,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.23420878,"top":0.97007185,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.24517952,"top":0.97007185,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.25631648,"top":0.97007185,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.26745346,"top":0.97007185,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.2785904,"top":0.97007185,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Sign In","depth":9,"bounds":{"left":0.75797874,"top":0.3990423,"width":0.13962767,"height":0.01915403},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Sign In","depth":10,"bounds":{"left":0.8159907,"top":0.39704707,"width":0.0234375,"height":0.023543496},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"google Sign in with Google","depth":9,"bounds":{"left":0.7719415,"top":0.45810056,"width":0.11170213,"height":0.031923383},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":true,"is_selected":false},{"role":"AXStaticText","text":"Sign in with Google","depth":11,"bounds":{"left":0.80701464,"top":0.46767756,"width":0.04155585,"height":0.0131683955},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"microsoft Sign in with Office 365","depth":9,"bounds":{"left":0.7719415,"top":0.5051876,"width":0.11170213,"height":0.031923383},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Sign in with Office 365","depth":11,"bounds":{"left":0.8033577,"top":0.51476455,"width":0.04886968,"height":0.0131683955},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Login via SSO","depth":9,"bounds":{"left":0.7719415,"top":0.5522745,"width":0.11170213,"height":0.031923383},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Login via SSO","depth":11,"bounds":{"left":0.8128325,"top":0.56185156,"width":0.029753989,"height":0.0131683955},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"EU","depth":9,"bounds":{"left":0.8040226,"top":0.6241022,"width":0.015458777,"height":0.028731046},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"EU","depth":10,"bounds":{"left":0.80867684,"top":0.63168395,"width":0.006150266,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Worldwide","depth":9,"bounds":{"left":0.8194814,"top":0.6241022,"width":0.032081116,"height":0.028731046},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-9712-change-forever-nudges-to-1-year-expiration ■ 874632","depth":10,"bounds":{"left":0.31166887,"top":0.9860335,"width":0.12333777,"height":0.012769354},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Don’t have account yet? Speak to your sales leader to get an invite.","depth":9,"bounds":{"left":0.7692819,"top":0.84038305,"width":0.116855055,"height":0.011971269},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"By using this service you agree to the","depth":9,"bounds":{"left":0.7631317,"top":0.8579409,"width":0.06615692,"height":0.011971269},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Privacy Policy","depth":9,"bounds":{"left":0.82928854,"top":0.8579409,"width":0.024933511,"height":0.011971269},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Privacy Policy","depth":10,"bounds":{"left":0.82928854,"top":0.8579409,"width":0.024933511,"height":0.011971269},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and","depth":9,"bounds":{"left":0.85422206,"top":0.8579409,"width":0.008144947,"height":0.011971269},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Terms of Service","depth":9,"bounds":{"left":0.86236703,"top":0.8579409,"width":0.029089095,"height":0.011971269},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Terms of Service","depth":10,"bounds":{"left":0.86236703,"top":0.8579409,"width":0.029089095,"height":0.011971269},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":9,"bounds":{"left":0.8914561,"top":0.8579409,"width":0.0009973404,"height":0.011971269},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"TERMS","depth":8,"bounds":{"left":0.77044547,"top":0.90901834,"width":0.028922873,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"TERMS","depth":9,"bounds":{"left":0.7770944,"top":0.90901834,"width":0.015625,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"KNOWLEDGE BASE","depth":8,"bounds":{"left":0.7997008,"top":0.90901834,"width":0.05618351,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"KNOWLEDGE BASE","depth":9,"bounds":{"left":0.80634975,"top":0.90901834,"width":0.04288564,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"BLOG","depth":8,"bounds":{"left":0.8562167,"top":0.90901834,"width":0.028756648,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"BLOG","depth":9,"bounds":{"left":0.8628657,"top":0.90901834,"width":0.012466756,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Open Intercom Messenger","depth":7,"bounds":{"left":0.9773936,"top":0.94573027,"width":0.015957447,"height":0.03830806},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Transferring data from uranus.staging.jiminny.com…","depth":5,"bounds":{"left":0.31233376,"top":0.9876297,"width":0.09042553,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-6193465694833207464
|
-9128134847420895493
|
visual_change
|
accessibility
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
New Tab
New Tab
AI reports promotion pages by nikolay-yankov · Pull Request #11998 · jiminny/app
AI reports promotion pages by nikolay-yankov · Pull Request #11998 · jiminny/app
Pull requests · jiminny/app
Pull requests · jiminny/app
Jiminny
Jiminny
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Sign In
Sign In
google Sign in with Google
Sign in with Google
microsoft Sign in with Office 365
Sign in with Office 365
Login via SSO
Login via SSO
EU
EU
Worldwide
JY-9712-change-forever-nudges-to-1-year-expiration ■ 874632
Don’t have account yet? Speak to your sales leader to get an invite.
By using this service you agree to the
Privacy Policy
Privacy Policy
and
Terms of Service
Terms of Service
.
TERMS
TERMS
KNOWLEDGE BASE
KNOWLEDGE BASE
BLOG
BLOG
Open Intercom Messenger
Transferring data from uranus.staging.jiminny.com…...
|
73127
|
|
73060
|
NULL
|
0
|
2026-04-23T06:45:01.901059+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776926701901_m1.jpg...
|
Firefox
|
Meet - Daily - Platform — Work
|
True
|
meet.google.com/agt-teir-cwt?authuser=lukas.kovali meet.google.com/agt-teir-cwt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - Daily - Platform
Close tab
New Tab
Open Goo Meet - Daily - Platform
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
2
Nikolay Yankov
Lukas Kovalik
9:45
AM
Daily - Platform
Daily - Platform
Audio settings
Turn off microphone
Video settings
Turn off camera
Share screen
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
You have joined the call. There is one other person in the call. Your camera is on. Your microphone is on. Your hand is lowered....
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - Daily - Platform","depth":4,"bounds":{"left":0.0,"top":0.072222225,"width":0.033680554,"height":0.045555554},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.0013888889,"top":0.072222225,"width":0.010416667,"height":0.016666668},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.005902778,"top":0.12,"width":0.022222223,"height":0.035555556},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.0,"top":0.7977778,"width":0.033680554,"height":0.043333333},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.0,"top":0.8411111,"width":0.033680554,"height":0.038333334},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.0,"top":0.8794444,"width":0.033680554,"height":0.03888889},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.0,"top":0.91833335,"width":0.033680554,"height":0.038333334},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0,"top":0.95666665,"width":0.033680554,"height":0.043333333},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"People","depth":14,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Nikolay Yankov","depth":17,"bounds":{"left":0.05659722,"top":1.0,"width":0.07673611,"height":-0.016111135},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Lukas Kovalik","depth":18,"bounds":{"left":0.8215278,"top":0.86777776,"width":0.052430555,"height":0.017222222},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"9:45","depth":12,"bounds":{"left":0.050347224,"top":0.9444444,"width":0.022569444,"height":0.022777777},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"AM","depth":12,"bounds":{"left":0.07638889,"top":0.9444444,"width":0.017708333,"height":0.022777777},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Daily - Platform","depth":12,"bounds":{"left":0.11145833,"top":0.9111111,"width":0.08055556,"height":0.08888888},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Daily - Platform","depth":15,"bounds":{"left":0.11145833,"top":0.9444444,"width":0.08055556,"height":0.022777777},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Audio settings","depth":13,"bounds":{"left":0.32118055,"top":0.9288889,"width":0.06111111,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn off microphone","depth":13,"bounds":{"left":0.34895834,"top":0.9288889,"width":0.033333335,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Video settings","depth":13,"bounds":{"left":0.38784721,"top":0.9288889,"width":0.06111111,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn off camera","depth":13,"bounds":{"left":0.415625,"top":0.9288889,"width":0.033333335,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Share screen","depth":12,"bounds":{"left":0.45451388,"top":0.9288889,"width":0.03888889,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Send a reaction","depth":12,"bounds":{"left":0.49895832,"top":0.9288889,"width":0.03888889,"height":0.053333335},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn on captions","depth":13,"bounds":{"left":0.5434028,"top":0.9288889,"width":0.03888889,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Raise hand (ctrl + ⌘ + h)","depth":12,"bounds":{"left":0.58784723,"top":0.9288889,"width":0.03888889,"height":0.053333335},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options","depth":12,"bounds":{"left":0.6322917,"top":0.9288889,"width":0.025,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Leave call","depth":12,"bounds":{"left":0.6628472,"top":0.9288889,"width":0.05,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Meeting details","depth":12,"bounds":{"left":0.89166665,"top":0.9288889,"width":0.033333335,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat with everyone","depth":12,"bounds":{"left":0.925,"top":0.9288889,"width":0.033333335,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Meeting tools","depth":12,"bounds":{"left":0.9583333,"top":0.9288889,"width":0.033333335,"height":0.053333335},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"You have joined the call. There is one other person in the call. Your camera is on. Your microphone is on. Your hand is lowered.","depth":8,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
691556998228832072
|
522613713079425924
|
visual_change
|
accessibility
|
NULL
|
Meet - Daily - Platform
Close tab
New Tab
Open Goo Meet - Daily - Platform
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
2
Nikolay Yankov
Lukas Kovalik
9:45
AM
Daily - Platform
Daily - Platform
Audio settings
Turn off microphone
Video settings
Turn off camera
Share screen
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
You have joined the call. There is one other person in the call. Your camera is on. Your microphone is on. Your hand is lowered....
|
NULL
|
|
73057
|
NULL
|
0
|
2026-04-23T06:44:51.627382+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776926691627_m2.jpg...
|
Firefox
|
Meet - Daily - Platform — Work
|
True
|
meet.google.com/agt-teir-cwt?authuser=lukas.kovali meet.google.com/agt-teir-cwt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - Daily - Platform
Close tab
New Tab
Open Goo Meet - Daily - Platform
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Return to home screen
[EMAIL]
Switch account
Switch account
Lukas Kovalik
More options
Turn off microphone
Turn off camera
Turn on background blur
Microphone: LakyLak bose qc35 II
Speaker: System Default Speaker Device
Camera: FaceTime HD Camera
Backgrounds and effects
Daily - Platform
Daily - Platform
Nikolay Yankov is in this call
Use Gemini to take notes Share notes and transcript
Use Gemini to take notes
Share notes and transcript
Start
Start
Join now
Join now
Other ways to join
Gemini is available in Meet as your personal in-meeting assistant. It can analyze conversation via temporary access to meeting captions. Using Ask Gemini won't create a recording or store meeting data. The meeting host can turn it off.
Learn more
Learn more...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - Daily - Platform","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.016123671,"height":-0.051875472},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.27094415,"top":1.0,"width":0.004986702,"height":-0.051875472},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.27310506,"top":1.0,"width":0.010638298,"height":-0.086193085},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Return to home screen","depth":10,"bounds":{"left":0.2917221,"top":1.0,"width":0.034906916,"height":-0.06464481},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"lukas.kovalik@jiminny.com","depth":12,"bounds":{"left":0.67204124,"top":1.0,"width":0.055684842,"height":-0.06584203},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Switch account","depth":11,"bounds":{"left":0.6981383,"top":1.0,"width":0.029587766,"height":-0.07901037},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Switch account","depth":12,"bounds":{"left":0.6981383,"top":1.0,"width":0.029587766,"height":-0.07901037},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Lukas Kovalik","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"More options","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn off microphone","depth":14,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Turn off camera","depth":14,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Turn on background blur","depth":13,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Microphone: LakyLak bose qc35 II","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Speaker: System Default Speaker Device","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Camera: FaceTime HD Camera","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Backgrounds and effects","depth":12,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Daily - Platform","depth":11,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Daily - Platform","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Nikolay Yankov is in this call","depth":13,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Use Gemini to take notes Share notes and transcript","depth":11,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Use Gemini to take notes","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Share notes and transcript","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Start","depth":12,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Start","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Join now","depth":12,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Join now","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Other ways to join","depth":12,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Gemini is available in Meet as your personal in-meeting assistant. It can analyze conversation via temporary access to meeting captions. Using Ask Gemini won't create a recording or store meeting data. The meeting host can turn it off.","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Learn more","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Learn more","depth":13,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
5375049518525522878
|
2236166021958061787
|
visual_change
|
accessibility
|
NULL
|
Meet - Daily - Platform
Close tab
New Tab
Open Goo Meet - Daily - Platform
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Return to home screen
[EMAIL]
Switch account
Switch account
Lukas Kovalik
More options
Turn off microphone
Turn off camera
Turn on background blur
Microphone: LakyLak bose qc35 II
Speaker: System Default Speaker Device
Camera: FaceTime HD Camera
Backgrounds and effects
Daily - Platform
Daily - Platform
Nikolay Yankov is in this call
Use Gemini to take notes Share notes and transcript
Use Gemini to take notes
Share notes and transcript
Start
Start
Join now
Join now
Other ways to join
Gemini is available in Meet as your personal in-meeting assistant. It can analyze conversation via temporary access to meeting captions. Using Ask Gemini won't create a recording or store meeting data. The meeting host can turn it off.
Learn more
Learn more...
|
NULL
|
|
73026
|
NULL
|
0
|
2026-04-23T06:39:38.601103+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776926378601_m1.jpg...
|
Slack
|
Nikolay Yankov (DM) - Jiminny Inc - 1 new item - S Nikolay Yankov (DM) - Jiminny Inc - 1 new item - Slack...
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
c-learning-people
confusion-clinic
curiosity_lab
deal-insights-dev
engineering
frontend
general
infra-changes
jiminny-bg
people-with-copilot-licences
people-with-zoom-phone-licences
platform-team
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Nikolay Yankov
Petko Kashinski
Stoyan Tomov
Aneliya Angelova
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Nikolay Nikolov
Mario Georgiev
Todor Stamatov
Gabriela Dureva
Vasil Vasilev
Galya Dimitrova
Stefka Stoyanova
Stoyan Tanev
Nikolay Ivanov
Jira Cloud
Toast
Messages
Messages
Add canvas
Add canvas
Files
Files
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Apr 21st at 4:38:16 PM
4:38
примерно?
Lukas Kovalik
Apr 21st at 4:40:18 PM
4:40 PM
да или /automated-reports/track-interest
Apr 21st at 4:40:43 PM
4:40
няма значение да си е POST
Apr 21st at 4:44:00 PM
4:44
гледам че си направил branch JY-20372-ai-reports-promotion-pages
Apr 21st at 4:44:06 PM
4:44
нали така, там да комитна
Nikolay Yankov
Apr 21st at 4:54:44 PM
4:54 PM
да, давай там
Jump to date
Nikolay Yankov
Yesterday at 10:06:05 AM
10:06 AM
Лукаш, има някакви коментари от Claude
виж ги ако мислиш, че трябва да се фиксне
https://github.com/jiminny/app/pull/11894
https://github.com/jiminny/app/pull/11894
Nikolay Yankov
Yesterday at 10:40:09 AM
10:40 AM
Lukas
, я виж каква промяна направих по BE, окей ли е?
https://github.com/jiminny/app/pull/11998/changes
https://github.com/jiminny/app/pull/11998/changes
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Yesterday at 10:40:20 AM
10:40
за Promotion pages
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Yesterday at 10:41:37 AM
10:41 AM
ей след малко
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Nikolay Yankov
Yesterday at 10:41:46 AM
10:41 AM
да, когато можеш
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Jump to date
Lukas Kovalik
Today at 9:25:47 AM
9:25 AM
добро утро, погледна промените ок са, но само да питам, че не съм сигурен къде ще се ползва
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 9:27:10 AM
9:27
hasGeneratedAiReports
в UserTransformer за киоск репорти ли е или за всички (включително ask Jiminny)
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Nikolay Yankov
Today at 9:38:49 AM
9:38 AM
за front-end-a e
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 9:39:10 AM
9:39
за AI Reports страницата
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Conversation marked as unread from this message
Nikolay Yankov is typing...
|
[{"role":"AXPopUpButton","text [{"role":"AXPopUpButton","text":"Switch workspaces… (Jiminny Inc) Has new messages","depth":14,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"Home","depth":14,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Home","depth":16,"role_description":"text"},{"role":"AXRadioButton","text":"DMs","depth":14,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"DMs","depth":16,"role_description":"text"},{"role":"AXRadioButton","text":"Activity","depth":14,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Activity","depth":16,"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":14,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":16,"role_description":"text"},{"role":"AXRadioButton","text":"Later","depth":14,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Later","depth":16,"role_description":"text"},{"role":"AXRadioButton","text":"More…","depth":14,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More","depth":16,"role_description":"text"},{"role":"AXStaticText","text":"Unreads","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Threads","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Huddles","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Drafts & sent","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Directories","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"jiminny-x-integration-app","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"platform-inner-team","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"ai-chapter","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"alerts","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"backend","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"c-learning-people","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"confusion-clinic","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"curiosity_lab","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"deal-insights-dev","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"engineering","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"frontend","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"general","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"infra-changes","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"jiminny-bg","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"people-with-copilot-licences","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"people-with-zoom-phone-licences","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"platform-team","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"platform-tickets","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"product_launches","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"random","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"releases","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"sofia-office","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"support","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"thank-yous","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"the_people_of_jiminny","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Yankov","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Petko Kashinski","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Stoyan Tomov","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":23,"role_description":"text"},{"role":"AXStaticText","text":",","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Yankov","depth":23,"role_description":"text"},{"role":"AXStaticText","text":",","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Steliyan Georgiev","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Nikolov","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Mario Georgiev","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Todor Stamatov","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Gabriela Dureva","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Vasil Vasilev","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Galya Dimitrova","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Stefka Stoyanova","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Stoyan Tanev","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Ivanov","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Jira Cloud","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Toast","depth":23,"role_description":"text"},{"role":"AXRadioButton","text":"Messages","depth":17,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Messages","depth":19,"role_description":"text"},{"role":"AXRadioButton","text":"Add canvas","depth":18,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Add canvas","depth":20,"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":17,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":19,"role_description":"text"},{"role":"AXPopUpButton","text":"Add and Edit Channel Tabs","depth":17,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Canvas","depth":17,"role_description":"text"},{"role":"AXStaticText","text":"List","depth":17,"role_description":"text"},{"role":"AXStaticText","text":"Folder","depth":17,"role_description":"text"},{"role":"AXPopUpButton","text":"Jump to date","depth":22,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Apr 21st at 4:38:16 PM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4:38","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"примерно?","depth":24,"role_description":"text"},{"role":"AXButton","text":"Lukas Kovalik","depth":23,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":23,"role_description":"text"},{"role":"AXLink","text":"Apr 21st at 4:40:18 PM","depth":23,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4:40 PM","depth":24,"role_description":"text"},{"role":"AXStaticText","text":"да или /automated-reports/track-interest","depth":24,"role_description":"text"},{"role":"AXLink","text":"Apr 21st at 4:40:43 PM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4:40","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"няма значение да си е POST","depth":24,"role_description":"text"},{"role":"AXLink","text":"Apr 21st at 4:44:00 PM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4:44","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"гледам че си направил branch JY-20372-ai-reports-promotion-pages","depth":24,"role_description":"text"},{"role":"AXLink","text":"Apr 21st at 4:44:06 PM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4:44","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"нали така, там да комитна","depth":24,"role_description":"text"},{"role":"AXButton","text":"Nikolay Yankov","depth":23,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":23,"role_description":"text"},{"role":"AXLink","text":"Apr 21st at 4:54:44 PM","depth":23,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4:54 PM","depth":24,"role_description":"text"},{"role":"AXStaticText","text":"да, давай там","depth":24,"role_description":"text"},{"role":"AXPopUpButton","text":"Jump to date","depth":22,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Nikolay Yankov","depth":23,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":23,"role_description":"text"},{"role":"AXLink","text":"Yesterday at 10:06:05 AM","depth":23,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:06 AM","depth":24,"role_description":"text"},{"role":"AXStaticText","text":"Лукаш, има някакви коментари от Claude","depth":24,"role_description":"text"},{"role":"AXStaticText","text":"виж ги ако мислиш, че трябва да се фиксне","depth":24,"role_description":"text"},{"role":"AXLink","text":"https://github.com/jiminny/app/pull/11894","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"https://github.com/jiminny/app/pull/11894","depth":25,"role_description":"text"},{"role":"AXButton","text":"Nikolay Yankov","depth":23,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":23,"role_description":"text"},{"role":"AXLink","text":"Yesterday at 10:40:09 AM","depth":23,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:40 AM","depth":24,"role_description":"text"},{"role":"AXStaticText","text":"Lukas","depth":24,"role_description":"text"},{"role":"AXStaticText","text":", я виж каква промяна направих по BE, окей ли е?","depth":24,"role_description":"text"},{"role":"AXLink","text":"https://github.com/jiminny/app/pull/11998/changes","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"https://github.com/jiminny/app/pull/11998/changes","depth":25,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":25,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Yesterday at 10:40:20 AM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:40","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"за Promotion pages","depth":24,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":25,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Lukas Kovalik","depth":23,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":23,"role_description":"text"},{"role":"AXLink","text":"Yesterday at 10:41:37 AM","depth":23,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:41 AM","depth":24,"role_description":"text"},{"role":"AXStaticText","text":"ей след малко","depth":24,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":25,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Nikolay Yankov","depth":23,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":23,"role_description":"text"},{"role":"AXLink","text":"Yesterday at 10:41:46 AM","depth":23,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:41 AM","depth":24,"role_description":"text"},{"role":"AXStaticText","text":"да, когато можеш","depth":24,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":25,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Jump to date","depth":22,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Lukas Kovalik","depth":23,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":23,"role_description":"text"},{"role":"AXLink","text":"Today at 9:25:47 AM","depth":23,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9:25 AM","depth":24,"role_description":"text"},{"role":"AXStaticText","text":"добро утро, погледна промените ок са, но само да питам, че не съм сигурен къде ще се ползва","depth":24,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":25,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 9:27:10 AM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9:27","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"hasGeneratedAiReports","depth":24,"role_description":"text"},{"role":"AXStaticText","text":"в UserTransformer за киоск репорти ли е или за всички (включително ask Jiminny)","depth":24,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":25,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Nikolay Yankov","depth":23,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":23,"role_description":"text"},{"role":"AXLink","text":"Today at 9:38:49 AM","depth":23,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9:38 AM","depth":24,"role_description":"text"},{"role":"AXStaticText","text":"за front-end-a e","depth":24,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":25,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 9:39:10 AM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9:39","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"за AI Reports страницата","depth":24,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":25,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":25,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"","depth":23,"value":"","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Conversation marked as unread from this message","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Yankov is typing","depth":11,"role_description":"text"}]...
|
7980557682795583329
|
-5892514131147839404
|
click
|
hybrid
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
c-learning-people
confusion-clinic
curiosity_lab
deal-insights-dev
engineering
frontend
general
infra-changes
jiminny-bg
people-with-copilot-licences
people-with-zoom-phone-licences
platform-team
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Nikolay Yankov
Petko Kashinski
Stoyan Tomov
Aneliya Angelova
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Nikolay Nikolov
Mario Georgiev
Todor Stamatov
Gabriela Dureva
Vasil Vasilev
Galya Dimitrova
Stefka Stoyanova
Stoyan Tanev
Nikolay Ivanov
Jira Cloud
Toast
Messages
Messages
Add canvas
Add canvas
Files
Files
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Apr 21st at 4:38:16 PM
4:38
примерно?
Lukas Kovalik
Apr 21st at 4:40:18 PM
4:40 PM
да или /automated-reports/track-interest
Apr 21st at 4:40:43 PM
4:40
няма значение да си е POST
Apr 21st at 4:44:00 PM
4:44
гледам че си направил branch JY-20372-ai-reports-promotion-pages
Apr 21st at 4:44:06 PM
4:44
нали така, там да комитна
Nikolay Yankov
Apr 21st at 4:54:44 PM
4:54 PM
да, давай там
Jump to date
Nikolay Yankov
Yesterday at 10:06:05 AM
10:06 AM
Лукаш, има някакви коментари от Claude
виж ги ако мислиш, че трябва да се фиксне
https://github.com/jiminny/app/pull/11894
https://github.com/jiminny/app/pull/11894
Nikolay Yankov
Yesterday at 10:40:09 AM
10:40 AM
Lukas
, я виж каква промяна направих по BE, окей ли е?
https://github.com/jiminny/app/pull/11998/changes
https://github.com/jiminny/app/pull/11998/changes
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Yesterday at 10:40:20 AM
10:40
за Promotion pages
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Yesterday at 10:41:37 AM
10:41 AM
ей след малко
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Nikolay Yankov
Yesterday at 10:41:46 AM
10:41 AM
да, когато можеш
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Jump to date
Lukas Kovalik
Today at 9:25:47 AM
9:25 AM
добро утро, погледна промените ок са, но само да питам, че не съм сигурен къде ще се ползва
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 9:27:10 AM
9:27
hasGeneratedAiReports
в UserTransformer за киоск репорти ли е или за всички (включително ask Jiminny)
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Nikolay Yankov
Today at 9:38:49 AM
9:38 AM
за front-end-a e
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 9:39:10 AM
9:39
за AI Reports страницата
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Conversation marked as unread from this message
Nikolay Yankov is typing
iTerm2ShellEditViewSessionScriptsProfilesWindowHelp•DOCKER• 881Source DB:NAS mount:Archive DB:Data dir:-zshО ₴2-zshOKOK(6.2G)/Volumes/Test/screenpipeexists (4.9G)OK(222 files, 255M)83* Build full day ac….• &4[+00m00s] • Counting source rowsfor2026-04-22frames:4305elements:332442ui_events:5804ocr_text:1074meetings:3[+00m01s] • Initialising tables, indexes, FTScreating tablescreating indexescreating FTS tables• Om00s• Om00s• OmOOs[+00m01s] • Syncing data for 2026-04-22video_chunksframes (4305 rows)ocr_text (1074 rows)ui_events (5804 rows)elements (332442 rows)meetings (3 rows)/ 0m01s/ 1m38s• 0m12s• Om01s• 0m51s• Om00s[+02m44s]• Updating FTSindexeselements_ftsframes_ftsui_events_fts• 1m26s• 2m14s• OmO2s[+06m26s] • Verifying DBframes:elements:ui_events:ocr_text:meetings:4305 / 4305332442 / 3324425804 / 58041074 / 10743 / 3[+07m20s] • Copying data folder for 2026-04-22rsync2026-04-22/ NAS• Om16s (222 files, 254M)[2026-04-23 09:23:38]Archive DB size:5.8G[2026-04-2309:23:387Total time:7m36s[2026-04-2309:23:38]Sync complete for 2026-04-22[2026-04-23 09:23:38]-____------пппппlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny~/.screenpipe/data/data/2026-04-22 $ I-zshscreenpipe*O 85Daily - Platform • in 6 mБГ100% <78Thu 23 Apr 9:39:40181-zsh886APP (-zsh)87ec2-user@ip-10-..• *8+...
|
73020
|
|
73025
|
NULL
|
0
|
2026-04-23T06:39:37.881700+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776926377881_m2.jpg...
|
Slack
|
Nikolay Yankov (DM) - Jiminny Inc - 1 new item - S Nikolay Yankov (DM) - Jiminny Inc - 1 new item - Slack...
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
c-learning-people
confusion-clinic
curiosity_lab
deal-insights-dev
engineering
frontend
general
infra-changes
jiminny-bg
people-with-copilot-licences
people-with-zoom-phone-licences
platform-team
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Nikolay Yankov
Petko Kashinski
Stoyan Tomov
Aneliya Angelova
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Nikolay Nikolov
Mario Georgiev
Todor Stamatov
Gabriela Dureva
Vasil Vasilev
Galya Dimitrova
Stefka Stoyanova
Stoyan Tanev
Nikolay Ivanov
Jira Cloud
Toast
Messages
Messages
Add canvas
Add canvas
Files
Files
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Apr 21st at 4:38:16 PM
4:38
примерно?
Lukas Kovalik
Apr 21st at 4:40:18 PM
4:40 PM
да или /automated-reports/track-interest
Apr 21st at 4:40:43 PM
4:40
няма значение да си е POST
Apr 21st at 4:44:00 PM
4:44
гледам че си направил branch JY-20372-ai-reports-promotion-pages
Apr 21st at 4:44:06 PM
4:44
нали така, там да комитна
Nikolay Yankov
Apr 21st at 4:54:44 PM
4:54 PM
да, давай там
Jump to date
Nikolay Yankov
Yesterday at 10:06:05 AM
10:06 AM
Лукаш, има някакви коментари от Claude
виж ги ако мислиш, че трябва да се фиксне
https://github.com/jiminny/app/pull/11894
https://github.com/jiminny/app/pull/11894
Nikolay Yankov
Yesterday at 10:40:09 AM
10:40 AM
Lukas
, я виж каква промяна направих по BE, окей ли е?
https://github.com/jiminny/app/pull/11998/changes
https://github.com/jiminny/app/pull/11998/changes
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Yesterday at 10:40:20 AM
10:40
за Promotion pages
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Yesterday at 10:41:37 AM
10:41 AM
ей след малко
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Nikolay Yankov
Yesterday at 10:41:46 AM
10:41 AM
да, когато можеш
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Jump to date
Lukas Kovalik
Today at 9:25:47 AM
9:25 AM
добро утро, погледна промените ок са, но само да питам, че не съм сигурен къде ще се ползва
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 9:27:10 AM
9:27
hasGeneratedAiReports
в UserTransformer за киоск репорти ли е или за всички (включително ask Jiminny)
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Nikolay Yankov
Today at 9:38:49 AM
9:38 AM
за front-end-a e
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 9:39:10 AM
9:39
за AI Reports страницата
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Conversation marked as unread from this message
Nikolay Yankov is typing...
|
[{"role":"AXPopUpButton","text [{"role":"AXPopUpButton","text":"Switch workspaces… (Jiminny Inc) Has new messages","depth":14,"bounds":{"left":0.0056515955,"top":0.058260176,"width":0.011968086,"height":0.028731046},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"Home","depth":14,"bounds":{"left":0.0029920214,"top":0.10055866,"width":0.017287234,"height":0.054269753},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Home","depth":16,"bounds":{"left":0.0066489363,"top":0.13806863,"width":0.009973404,"height":0.0103751},"role_description":"text"},{"role":"AXRadioButton","text":"DMs","depth":14,"bounds":{"left":0.0029920214,"top":0.15482841,"width":0.017287234,"height":0.054269753},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"DMs","depth":16,"bounds":{"left":0.0076462766,"top":0.19233839,"width":0.007978723,"height":0.0103751},"role_description":"text"},{"role":"AXRadioButton","text":"Activity","depth":14,"bounds":{"left":0.0029920214,"top":0.20909816,"width":0.017287234,"height":0.054269753},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Activity","depth":16,"bounds":{"left":0.004986702,"top":0.24660814,"width":0.012965426,"height":0.0103751},"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":14,"bounds":{"left":0.0029920214,"top":0.26336792,"width":0.017287234,"height":0.054269753},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":16,"bounds":{"left":0.0076462766,"top":0.3008779,"width":0.0076462766,"height":0.0103751},"role_description":"text"},{"role":"AXRadioButton","text":"Later","depth":14,"bounds":{"left":0.0029920214,"top":0.31763768,"width":0.017287234,"height":0.054269753},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Later","depth":16,"bounds":{"left":0.00731383,"top":0.35514766,"width":0.008643617,"height":0.0103751},"role_description":"text"},{"role":"AXRadioButton","text":"More…","depth":14,"bounds":{"left":0.0029920214,"top":0.3719074,"width":0.017287234,"height":0.054269753},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More","depth":16,"bounds":{"left":0.006981383,"top":0.4094174,"width":0.008976064,"height":0.0103751},"role_description":"text"},{"role":"AXStaticText","text":"Unreads","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Threads","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Huddles","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Drafts & sent","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Directories","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"jiminny-x-integration-app","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"platform-inner-team","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"ai-chapter","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"alerts","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"backend","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"c-learning-people","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"confusion-clinic","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"curiosity_lab","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"deal-insights-dev","depth":23,"bounds":{"left":0.042220745,"top":0.096568234,"width":0.03723404,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"engineering","depth":23,"bounds":{"left":0.042220745,"top":0.118914604,"width":0.025598405,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"frontend","depth":23,"bounds":{"left":0.042220745,"top":0.14126097,"width":0.018949468,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"general","depth":23,"bounds":{"left":0.042220745,"top":0.16360734,"width":0.015957447,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"infra-changes","depth":23,"bounds":{"left":0.042220745,"top":0.1859537,"width":0.029587766,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"jiminny-bg","depth":23,"bounds":{"left":0.042220745,"top":0.20830008,"width":0.022938829,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"people-with-copilot-licences","depth":23,"bounds":{"left":0.042220745,"top":0.23064645,"width":0.045212764,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"people-with-zoom-phone-licences","depth":23,"bounds":{"left":0.042220745,"top":0.2529928,"width":0.045877658,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"platform-team","depth":23,"bounds":{"left":0.042220745,"top":0.2753392,"width":0.03125,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"platform-tickets","depth":23,"bounds":{"left":0.042220745,"top":0.29768556,"width":0.034906916,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"product_launches","depth":23,"bounds":{"left":0.042220745,"top":0.3200319,"width":0.03856383,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"random","depth":23,"bounds":{"left":0.042220745,"top":0.3423783,"width":0.01662234,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"releases","depth":23,"bounds":{"left":0.042220745,"top":0.36472467,"width":0.01761968,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"sofia-office","depth":23,"bounds":{"left":0.042220745,"top":0.38707104,"width":0.024268618,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"support","depth":23,"bounds":{"left":0.042220745,"top":0.4094174,"width":0.016954787,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"thank-yous","depth":23,"bounds":{"left":0.042220745,"top":0.43176377,"width":0.024268618,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"the_people_of_jiminny","depth":23,"bounds":{"left":0.042220745,"top":0.45411015,"width":0.04488032,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Yankov","depth":23,"bounds":{"left":0.042220745,"top":0.5067837,"width":0.032912236,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Petko Kashinski","depth":23,"bounds":{"left":0.042220745,"top":0.5291301,"width":0.034242023,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Stoyan Tomov","depth":23,"bounds":{"left":0.042220745,"top":0.5514765,"width":0.030585106,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":23,"bounds":{"left":0.042220745,"top":0.5738228,"width":0.03756649,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":23,"bounds":{"left":0.042220745,"top":0.5961692,"width":0.03756649,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":",","depth":23,"bounds":{"left":0.07945479,"top":0.5961692,"width":0.0063164895,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Yankov","depth":23,"bounds":{"left":0.08211436,"top":0.5961692,"width":0.014295213,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":",","depth":23,"bounds":{"left":0.09607713,"top":0.61372703,"width":0.0003324468,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Steliyan Georgiev","depth":23,"bounds":{"left":0.09607713,"top":0.61372703,"width":0.0003324468,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Nikolov","depth":23,"bounds":{"left":0.042220745,"top":0.61851555,"width":0.034242023,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Mario Georgiev","depth":23,"bounds":{"left":0.042220745,"top":0.6408619,"width":0.033909574,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Todor Stamatov","depth":23,"bounds":{"left":0.042220745,"top":0.6632083,"width":0.034242023,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Gabriela Dureva","depth":23,"bounds":{"left":0.042220745,"top":0.6855547,"width":0.03523936,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Vasil Vasilev","depth":23,"bounds":{"left":0.042220745,"top":0.70790106,"width":0.026263298,"height":0.0015961692},"role_description":"text"},{"role":"AXStaticText","text":"Galya Dimitrova","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.034906916,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Stefka Stoyanova","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.03756649,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Stoyan Tanev","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.028922873,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Ivanov","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.031914894,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Jira Cloud","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.021609042,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Toast","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.011635638,"height":0.0007980846},"role_description":"text"},{"role":"AXRadioButton","text":"Messages","depth":17,"bounds":{"left":0.10206117,"top":0.09177973,"width":0.030585106,"height":0.030327214},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Messages","depth":19,"bounds":{"left":0.111369684,"top":0.10055866,"width":0.01861702,"height":0.012769354},"role_description":"text"},{"role":"AXRadioButton","text":"Add canvas","depth":18,"bounds":{"left":0.13397606,"top":0.09177973,"width":0.033909574,"height":0.030327214},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Add canvas","depth":20,"bounds":{"left":0.14328457,"top":0.10055866,"width":0.021941489,"height":0.012769354},"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":17,"bounds":{"left":0.16921543,"top":0.09177973,"width":0.020944148,"height":0.030327214},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":19,"bounds":{"left":0.17852394,"top":0.10055866,"width":0.008976064,"height":0.012769354},"role_description":"text"},{"role":"AXPopUpButton","text":"Add and Edit Channel Tabs","depth":17,"bounds":{"left":0.19115691,"top":0.09177973,"width":0.010970744,"height":0.030327214},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Canvas","depth":17,"bounds":{"left":0.096409574,"top":0.0518755,"width":0.015625,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"List","depth":17,"bounds":{"left":0.096409574,"top":0.0518755,"width":0.0076462766,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Folder","depth":17,"bounds":{"left":0.096409574,"top":0.0518755,"width":0.013962766,"height":0.0007980846},"role_description":"text"},{"role":"AXPopUpButton","text":"Jump to date","depth":22,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Apr 21st at 4:38:16 PM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4:38","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"примерно?","depth":24,"role_description":"text"},{"role":"AXButton","text":"Lukas Kovalik","depth":23,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":23,"role_description":"text"},{"role":"AXLink","text":"Apr 21st at 4:40:18 PM","depth":23,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4:40 PM","depth":24,"role_description":"text"},{"role":"AXStaticText","text":"да или /automated-reports/track-interest","depth":24,"role_description":"text"},{"role":"AXLink","text":"Apr 21st at 4:40:43 PM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4:40","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"няма значение да си е POST","depth":24,"role_description":"text"},{"role":"AXLink","text":"Apr 21st at 4:44:00 PM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4:44","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"гледам че си направил branch JY-20372-ai-reports-promotion-pages","depth":24,"role_description":"text"},{"role":"AXLink","text":"Apr 21st at 4:44:06 PM","depth":24,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4:44","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"нали така, там да комитна","depth":24,"role_description":"text"},{"role":"AXButton","text":"Nikolay Yankov","depth":23,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":23,"role_description":"text"},{"role":"AXLink","text":"Apr 21st at 4:54:44 PM","depth":23,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4:54 PM","depth":24,"role_description":"text"},{"role":"AXStaticText","text":"да, давай там","depth":24,"role_description":"text"},{"role":"AXPopUpButton","text":"Jump to date","depth":22,"bounds":{"left":0.14660904,"top":0.12689546,"width":0.032579787,"height":0.022346368},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Nikolay Yankov","depth":23,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":23,"role_description":"text"},{"role":"AXLink","text":"Yesterday at 10:06:05 AM","depth":23,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:06 AM","depth":24,"role_description":"text"},{"role":"AXStaticText","text":"Лукаш, има някакви коментари от Claude","depth":24,"role_description":"text"},{"role":"AXStaticText","text":"виж ги ако мислиш, че трябва да се фиксне","depth":24,"bounds":{"left":0.11801862,"top":0.11731844,"width":0.10006649,"height":0.015163607},"role_description":"text"},{"role":"AXLink","text":"https://github.com/jiminny/app/pull/11894","depth":24,"bounds":{"left":0.11801862,"top":0.1348763,"width":0.09474734,"height":0.015163607},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"https://github.com/jiminny/app/pull/11894","depth":25,"bounds":{"left":0.11801862,"top":0.1348763,"width":0.09474734,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Nikolay Yankov","depth":23,"bounds":{"left":0.11801862,"top":0.15722266,"width":0.034242023,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":23,"bounds":{"left":0.15226063,"top":0.15881884,"width":0.0026595744,"height":0.015163607},"role_description":"text"},{"role":"AXLink","text":"Yesterday at 10:40:09 AM","depth":23,"bounds":{"left":0.1549202,"top":0.16121309,"width":0.01761968,"height":0.011971269},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:40 AM","depth":24,"bounds":{"left":0.1549202,"top":0.16121309,"width":0.01761968,"height":0.011971269},"role_description":"text"},{"role":"AXStaticText","text":"Lukas","depth":24,"bounds":{"left":0.11801862,"top":0.1763767,"width":0.012300532,"height":0.015163607},"role_description":"text"},{"role":"AXStaticText","text":", я виж каква промяна направих по BE, окей ли е?","depth":24,"bounds":{"left":0.11801862,"top":0.1763767,"width":0.100398935,"height":0.032721467},"role_description":"text"},{"role":"AXLink","text":"https://github.com/jiminny/app/pull/11998/changes","depth":24,"bounds":{"left":0.11801862,"top":0.21149242,"width":0.1043883,"height":0.032721467},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"https://github.com/jiminny/app/pull/11998/changes","depth":25,"bounds":{"left":0.11801862,"top":0.21149242,"width":0.1043883,"height":0.032721467},"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":25,"bounds":{"left":0.13730054,"top":0.14365523,"width":0.010638298,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":25,"bounds":{"left":0.14793883,"top":0.14365523,"width":0.010638298,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":25,"bounds":{"left":0.15857713,"top":0.14365523,"width":0.010638298,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":25,"bounds":{"left":0.16921543,"top":0.14365523,"width":0.010638298,"height":0.026336791},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":25,"bounds":{"left":0.17985372,"top":0.14365523,"width":0.010638298,"height":0.026336791},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":25,"bounds":{"left":0.22340426,"top":0.14365523,"width":0.0003324468,"height":0.026336791},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":25,"bounds":{"left":0.22340426,"top":0.14365523,"width":0.0003324468,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":25,"bounds":{"left":0.22340426,"top":0.14365523,"width":0.0003324468,"height":0.026336791},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Yesterday at 10:40:20 AM","depth":24,"bounds":{"left":0.105053194,"top":0.25538707,"width":0.010305851,"height":0.011971269},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:40","depth":25,"bounds":{"left":0.105053194,"top":0.25538707,"width":0.010305851,"height":0.011971269},"role_description":"text"},{"role":"AXStaticText","text":"за Promotion pages","depth":24,"bounds":{"left":0.11801862,"top":0.2529928,"width":0.04288564,"height":0.015163607},"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":25,"bounds":{"left":0.13730054,"top":0.22825219,"width":0.010638298,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":25,"bounds":{"left":0.14793883,"top":0.22825219,"width":0.010638298,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":25,"bounds":{"left":0.15857713,"top":0.22825219,"width":0.010638298,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":25,"bounds":{"left":0.16921543,"top":0.22825219,"width":0.010638298,"height":0.026336791},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":25,"bounds":{"left":0.17985372,"top":0.22825219,"width":0.010638298,"height":0.026336791},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":25,"bounds":{"left":0.22340426,"top":0.22825219,"width":0.0003324468,"height":0.026336791},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":25,"bounds":{"left":0.22340426,"top":0.22825219,"width":0.0003324468,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":25,"bounds":{"left":0.22340426,"top":0.22825219,"width":0.0003324468,"height":0.026336791},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Lukas Kovalik","depth":23,"bounds":{"left":0.11801862,"top":0.2753392,"width":0.030917553,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":23,"bounds":{"left":0.14860372,"top":0.27693537,"width":0.0029920214,"height":0.015163607},"role_description":"text"},{"role":"AXLink","text":"Yesterday at 10:41:37 AM","depth":23,"bounds":{"left":0.1512633,"top":0.2793296,"width":0.01761968,"height":0.011971269},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:41 AM","depth":24,"bounds":{"left":0.1512633,"top":0.2793296,"width":0.01761968,"height":0.011971269},"role_description":"text"},{"role":"AXStaticText","text":"ей след малко","depth":24,"bounds":{"left":0.11801862,"top":0.29449323,"width":0.03324468,"height":0.015163607},"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":25,"bounds":{"left":0.13730054,"top":0.26177174,"width":0.010638298,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":25,"bounds":{"left":0.14793883,"top":0.26177174,"width":0.010638298,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":25,"bounds":{"left":0.15857713,"top":0.26177174,"width":0.010638298,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":25,"bounds":{"left":0.16921543,"top":0.26177174,"width":0.010638298,"height":0.026336791},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":25,"bounds":{"left":0.17985372,"top":0.26177174,"width":0.010638298,"height":0.026336791},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":25,"bounds":{"left":0.22340426,"top":0.26177174,"width":0.0003324468,"height":0.026336791},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":25,"bounds":{"left":0.22340426,"top":0.26177174,"width":0.0003324468,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":25,"bounds":{"left":0.22340426,"top":0.26177174,"width":0.0003324468,"height":0.026336791},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Nikolay Yankov","depth":23,"bounds":{"left":0.11801862,"top":0.31683958,"width":0.034242023,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":23,"bounds":{"left":0.15226063,"top":0.31843576,"width":0.0026595744,"height":0.015163607},"role_description":"text"},{"role":"AXLink","text":"Yesterday at 10:41:46 AM","depth":23,"bounds":{"left":0.1549202,"top":0.32083002,"width":0.01761968,"height":0.011971269},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:41 AM","depth":24,"bounds":{"left":0.1549202,"top":0.32083002,"width":0.01761968,"height":0.011971269},"role_description":"text"},{"role":"AXStaticText","text":"да, когато можеш","depth":24,"bounds":{"left":0.11801862,"top":0.33599362,"width":0.04089096,"height":0.015163607},"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":25,"bounds":{"left":0.13730054,"top":0.30327216,"width":0.010638298,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":25,"bounds":{"left":0.14793883,"top":0.30327216,"width":0.010638298,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":25,"bounds":{"left":0.15857713,"top":0.30327216,"width":0.010638298,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":25,"bounds":{"left":0.16921543,"top":0.30327216,"width":0.010638298,"height":0.026336791},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":25,"bounds":{"left":0.17985372,"top":0.30327216,"width":0.010638298,"height":0.026336791},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":25,"bounds":{"left":0.22340426,"top":0.30327216,"width":0.0003324468,"height":0.026336791},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":25,"bounds":{"left":0.22340426,"top":0.30327216,"width":0.0003324468,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":25,"bounds":{"left":0.22340426,"top":0.30327216,"width":0.0003324468,"height":0.026336791},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Jump to date","depth":22,"bounds":{"left":0.15026596,"top":0.36632082,"width":0.025265958,"height":0.023144454},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Lukas Kovalik","depth":23,"bounds":{"left":0.11801862,"top":0.39744613,"width":0.030917553,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":23,"bounds":{"left":0.14860372,"top":0.3990423,"width":0.0029920214,"height":0.015163607},"role_description":"text"},{"role":"AXLink","text":"Today at 9:25:47 AM","depth":23,"bounds":{"left":0.1512633,"top":0.40143654,"width":0.015292553,"height":0.011971269},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9:25 AM","depth":24,"bounds":{"left":0.1512633,"top":0.40143654,"width":0.015292553,"height":0.011971269},"role_description":"text"},{"role":"AXStaticText","text":"добро утро, погледна промените ок са, но само да питам, че не съм сигурен къде ще се ползва","depth":24,"bounds":{"left":0.11801862,"top":0.41660017,"width":0.10405585,"height":0.05027933},"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":25,"bounds":{"left":0.13730054,"top":0.38387868,"width":0.010638298,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":25,"bounds":{"left":0.14793883,"top":0.38387868,"width":0.010638298,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":25,"bounds":{"left":0.15857713,"top":0.38387868,"width":0.010638298,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":25,"bounds":{"left":0.16921543,"top":0.38387868,"width":0.010638298,"height":0.026336791},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":25,"bounds":{"left":0.17985372,"top":0.38387868,"width":0.010638298,"height":0.026336791},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":25,"bounds":{"left":0.22340426,"top":0.38387868,"width":0.0003324468,"height":0.026336791},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":25,"bounds":{"left":0.22340426,"top":0.38387868,"width":0.0003324468,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":25,"bounds":{"left":0.22340426,"top":0.38387868,"width":0.0003324468,"height":0.026336791},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 9:27:10 AM","depth":24,"bounds":{"left":0.107380316,"top":0.47805268,"width":0.007978723,"height":0.011971269},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9:27","depth":25,"bounds":{"left":0.107380316,"top":0.47805268,"width":0.007978723,"height":0.011971269},"role_description":"text"},{"role":"AXStaticText","text":"hasGeneratedAiReports","depth":24,"bounds":{"left":0.12101064,"top":0.48603353,"width":0.050199468,"height":0.011971269},"role_description":"text"},{"role":"AXStaticText","text":"в UserTransformer за киоск репорти ли е или за всички (включително ask Jiminny)","depth":24,"bounds":{"left":0.11801862,"top":0.51077414,"width":0.10172872,"height":0.032721467},"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":25,"bounds":{"left":0.13730054,"top":0.4509178,"width":0.010638298,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":25,"bounds":{"left":0.14793883,"top":0.4509178,"width":0.010638298,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":25,"bounds":{"left":0.15857713,"top":0.4509178,"width":0.010638298,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":25,"bounds":{"left":0.16921543,"top":0.4509178,"width":0.010638298,"height":0.026336791},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":25,"bounds":{"left":0.17985372,"top":0.4509178,"width":0.010638298,"height":0.026336791},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":25,"bounds":{"left":0.22340426,"top":0.4509178,"width":0.0003324468,"height":0.026336791},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":25,"bounds":{"left":0.22340426,"top":0.4509178,"width":0.0003324468,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":25,"bounds":{"left":0.22340426,"top":0.4509178,"width":0.0003324468,"height":0.026336791},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Nikolay Yankov","depth":23,"bounds":{"left":0.11801862,"top":0.5506784,"width":0.034242023,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":23,"bounds":{"left":0.15226063,"top":0.5522745,"width":0.0026595744,"height":0.015163607},"role_description":"text"},{"role":"AXLink","text":"Today at 9:38:49 AM","depth":23,"bounds":{"left":0.1549202,"top":0.5546688,"width":0.015292553,"height":0.011971269},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9:38 AM","depth":24,"bounds":{"left":0.1549202,"top":0.5546688,"width":0.015292553,"height":0.011971269},"role_description":"text"},{"role":"AXStaticText","text":"за front-end-a e","depth":24,"bounds":{"left":0.11801862,"top":0.5698324,"width":0.03523936,"height":0.015163607},"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":25,"bounds":{"left":0.13730054,"top":0.5371109,"width":0.010638298,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":25,"bounds":{"left":0.14793883,"top":0.5371109,"width":0.010638298,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":25,"bounds":{"left":0.15857713,"top":0.5371109,"width":0.010638298,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":25,"bounds":{"left":0.16921543,"top":0.5371109,"width":0.010638298,"height":0.026336791},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":25,"bounds":{"left":0.17985372,"top":0.5371109,"width":0.010638298,"height":0.026336791},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":25,"bounds":{"left":0.22340426,"top":0.5371109,"width":0.0003324468,"height":0.026336791},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":25,"bounds":{"left":0.22340426,"top":0.5371109,"width":0.0003324468,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":25,"bounds":{"left":0.22340426,"top":0.5371109,"width":0.0003324468,"height":0.026336791},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 9:39:10 AM","depth":24,"bounds":{"left":0.107380316,"top":0.5961692,"width":0.007978723,"height":0.011971269},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9:39","depth":25,"bounds":{"left":0.107380316,"top":0.5961692,"width":0.007978723,"height":0.011971269},"role_description":"text"},{"role":"AXStaticText","text":"за AI Reports страницата","depth":24,"bounds":{"left":0.11801862,"top":0.5937749,"width":0.05651596,"height":0.015163607},"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":25,"bounds":{"left":0.13730054,"top":0.56903434,"width":0.010638298,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":25,"bounds":{"left":0.14793883,"top":0.56903434,"width":0.010638298,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":25,"bounds":{"left":0.15857713,"top":0.56903434,"width":0.010638298,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":25,"bounds":{"left":0.16921543,"top":0.56903434,"width":0.010638298,"height":0.026336791},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":25,"bounds":{"left":0.17985372,"top":0.56903434,"width":0.010638298,"height":0.026336791},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":25,"bounds":{"left":0.22340426,"top":0.56903434,"width":0.0003324468,"height":0.026336791},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":25,"bounds":{"left":0.22340426,"top":0.56903434,"width":0.0003324468,"height":0.026336791},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":25,"bounds":{"left":0.22340426,"top":0.56903434,"width":0.0003324468,"height":0.026336791},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"","depth":23,"bounds":{"left":0.10372341,"top":0.6272945,"width":0.118351065,"height":0.030327214},"value":"","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Conversation marked as unread from this message","depth":11,"bounds":{"left":0.0,"top":0.7126895,"width":0.02825798,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Yankov is typing","depth":11,"bounds":{"left":0.0,"top":0.7126895,"width":0.015625,"height":0.0007980846},"role_description":"text"}]...
|
7980557682795583329
|
-5892514131147839404
|
click
|
hybrid
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
c-learning-people
confusion-clinic
curiosity_lab
deal-insights-dev
engineering
frontend
general
infra-changes
jiminny-bg
people-with-copilot-licences
people-with-zoom-phone-licences
platform-team
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Nikolay Yankov
Petko Kashinski
Stoyan Tomov
Aneliya Angelova
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Nikolay Nikolov
Mario Georgiev
Todor Stamatov
Gabriela Dureva
Vasil Vasilev
Galya Dimitrova
Stefka Stoyanova
Stoyan Tanev
Nikolay Ivanov
Jira Cloud
Toast
Messages
Messages
Add canvas
Add canvas
Files
Files
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Apr 21st at 4:38:16 PM
4:38
примерно?
Lukas Kovalik
Apr 21st at 4:40:18 PM
4:40 PM
да или /automated-reports/track-interest
Apr 21st at 4:40:43 PM
4:40
няма значение да си е POST
Apr 21st at 4:44:00 PM
4:44
гледам че си направил branch JY-20372-ai-reports-promotion-pages
Apr 21st at 4:44:06 PM
4:44
нали така, там да комитна
Nikolay Yankov
Apr 21st at 4:54:44 PM
4:54 PM
да, давай там
Jump to date
Nikolay Yankov
Yesterday at 10:06:05 AM
10:06 AM
Лукаш, има някакви коментари от Claude
виж ги ако мислиш, че трябва да се фиксне
https://github.com/jiminny/app/pull/11894
https://github.com/jiminny/app/pull/11894
Nikolay Yankov
Yesterday at 10:40:09 AM
10:40 AM
Lukas
, я виж каква промяна направих по BE, окей ли е?
https://github.com/jiminny/app/pull/11998/changes
https://github.com/jiminny/app/pull/11998/changes
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Yesterday at 10:40:20 AM
10:40
за Promotion pages
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Yesterday at 10:41:37 AM
10:41 AM
ей след малко
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Nikolay Yankov
Yesterday at 10:41:46 AM
10:41 AM
да, когато можеш
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Jump to date
Lukas Kovalik
Today at 9:25:47 AM
9:25 AM
добро утро, погледна промените ок са, но само да питам, че не съм сигурен къде ще се ползва
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 9:27:10 AM
9:27
hasGeneratedAiReports
в UserTransformer за киоск репорти ли е или за всички (включително ask Jiminny)
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Nikolay Yankov
Today at 9:38:49 AM
9:38 AM
за front-end-a e
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 9:39:10 AM
9:39
за AI Reports страницата
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Conversation marked as unread from this message
Nikolay Yankov is typing
HomeMoreSlackcalVIewMistonWindowhelt~ Describe wnat you are lookins forJiminny ...# deal-insights-dev# engineeringa trontend# general# infra-changes# jiminny-bg• people-with-copilo..8 people-with-zoom-…# platform-team# platform-tickets# product launches# randomi released# sofia-office# support# thank-yous# the people of iimi..A Direct messagesNikolay Yankov• Petko KashinskiStoyan TomovAneliva Angelova3 Aneliya Angelova,0. Nikolay Nikolovf Mario GeorgievSg: Todor Stamatov• Gabriela DurevaNikolay Yankov• Messagest Add canvaur FilesВИЖ ГИ АКО Мислиш но тлябва да се фикснеapp/pull/11894Nikolav Yankov 10:40 AMLukas, я виж каква промяна направих по ВЕ,окей ли е?https://github.com/jiminny/app/pull/11998/cha3а Promotion pagesLukas Kovalik 10:41 AMей слел малкоNikolav Yankov 10:41 AMла, когато можешLukas Kovallik 9.25 AMдобро утро, погледна промените ок са, носамо да питам, че не съм сигурен къде ще сеnoлapаhasGeneratedAlReportsp Ucer ranctormer за киоск пепопти пи е илиза всички (включително ask Jiminny)Nikolav Yankov 9.38 AMaa front-end-a eaa Al Reports страницатаMessage Nikolay Yankos+ AalFV faVsco.js°9 JY-20157-AJ-report-not-send-notificaProject© EventServiceProvider.php© HubspotJournalServiceProvider.p(C) HubsnotWebhookServiceProvider© JiminnyServiceProvider.php(c) PlanhatServiceProvider.ong© ProphetHandlerServiceProvider.p© PusherServiceProvider.php29.07.25 NIkolov© QueueLogServiceProvider.php3.09.25© QueueStatsdServiceProvider.php3.09.25vanov© ResponseMacroServiceProvider.r3.09.25 Ivanov© RouteServiceProvider.php3.09.25Ivano© SsoServiceProvider.php3.09.25 Ivanov© UtilServiceProvider.php3.09.25 Ivanov•AskJlminnykeporacuvityservice.org© AutomatedReportsCommand.php-repori-not-generated.dlade.png©) PlannatActivityListener.phpAutomatedreporiskepository.ohopnp api vz.ohpclass Automatedreportsrepos1toryA16 ×7 21 ^ououc runction countuserrevortsluser suser): 1ntPeромо, tOncoIon Ci ose (oose1'team1d'. suser->cet.reamidobi'recinients-›users', Suser->getidon:->counto:c) ViewercuaroserviceProvider.ono19.09.25 Nikolov282> D Queue21.04.26 Kovalikv → Repositories>Ai> → AutoScorina21.04.26 Kovalik284* Restrict a query on the automated_reports table to reports the given user is allov 14021.04.26 Kovalik21.04.26 Kovalik>@ Calendar21.04 26 Kovalikvermx nucches ene coscomer-racane avesence.explicit user recipients (recipients.users)members of.any.of the report's groups (Ask Jiminny reports)© AccountRepository.php21.04.26 Kovalik21.04.26 Kovalik© ContactRepository.php© ContactRoleRepository.php210A26 Kovalil© CrmConfigurationRepository.p21.04.26 Kovalik© CrmEntityRepository.php21.04.26 Kovalik© FieldDataRepository.php21.04.26 Kovalik© FieldRepository.php21.04.26 Kovalik© LayoutEntityRepository.php21.04.26 Kovalik© LayoutRepository.php21.04.26 KovallkC) LeadRepository.pnp21.04.20 Kovallk© OpportunityRepository.php74.04.26 Kovallk© ProfileRepository.php21.04.26 Kovalik© RecordTypeFieldValuesReposi21.04.26 Kovalik© StageRepository.php21.04.26 Kovalik© SyncBatchRepository.php> C Geography21.04.26 Kovalik21.04.26 Kovalik© ActiveStreamsRepositorv.php71.04.26 Kovallk© ActivityCommentRepository.php71.04.26 Kovallk© ActivityLogRepository.php21.04.26 Kovalik© ActivitvMessageRepository.php21.04.26 Kovalik@ActivitvMomentRepositorv.oh21.04.26 Kovalik@ActivitvProviderRepositorv.ohp19.09.25 Nikolov@ActivitvRepositorv.ohn19.09.25 Nikolov@ActivitvSearchFilterRepositorv.oh10 00 25 Nikolow@ ActivitvShareRepositorv.ohr10.00 25 Nikolol© ActivityUploadSettingRepository-i10.00 25 Nikolol© AiPromptRepository.php@AckAnvthinaRenository.nhr19.09.25 Nikolov10.00 25 Nikolow1usageprivate function applyUserAccessScope(Builder $query, User $user): voidSuserId = Suser->getId®:saroupld = suser->getbroupldonSquery->where column:'automated_reports.team 1d', suser->qetleamldoo->where tunction (Bullder so use (suserid, saroupld: vo1dif (SaroupId |== null) {So->orWereGfunction Builder Ssub) use (Saroupid): void <'automated reponts tvpe'. operator: AutomatedRe 231->whereJsonContainsa'automated reports.aroups'. Saroul309* Get report IDs for a specific team* Ananam Toam Stean* Aneturn \Illuminate Support Collection9 AutomatedReportsRepository.php@ CallimportRepository.php(C) CoachinaFeedhackRenasitorv nhil5 usages10 00 26 AiLalaspublic function getReportIdsByTeam(Team Steam): Illuminate Support\Collection{...}19.09.25 NikolovDally - Platrorm• In om100% 152Inu Z3 Aor 9:39:30+0 •= custom.log= laravel.logA SF [jiminny@localhost]# console PROD© DealsRepository.php x& console (EUA36 ^— 141=144pobese tunceson ye146->orderByscriter15return $qb->getO->all154155 @› ic function getStages(180 @> ic function getConfigu1197188› ic function getPipelinato Gunation enostonipplies all applicablereturn stringll,2ee funation nudratectparam intl] $dealids, te sunntion fatoncteCascadeAutomated Reports Lrecioients->users contains userBut AutomatedRenortsRepositorv.oho:290-307 (applvUserAccessScone) checks:• EITHER recipients->users contains user OR (Ask Jiminny AND user in groups )Problem: countUserReports misses Ask Jiminny reports where user has access via aroup membershio (not recioients). ThisScenario AnalysisScenario A: User with onlv automated reports (no Ask Jiminny))•Access via applyUserAccessScope: No access• Not in recipients→>users automated reports don't use this tield)• Not Ask liminnv. so aroun check doesn't anolv• BUT if you pass their groupId in the teamIds filter parameter: ™ Will show• Lines 252-259 filter by groups JSON field regardless of report typeScenario B: User A in Group A. Non-ASK Jiminny report with groups = A, B)Access via applyUserAccessScope: No access•Not ASK Jiminny, so groups field is ignored tor access controlUser must be in recinients-susers (which non-Ask liminnv doesn't use)With teamIds filter: V Will show• If teanids-IAl is nassed. lines 252-259 will matchScenario C: User in group for Ask Jiminny report, but user doesn't have "Ask Jiminny enabled"•Access via applyUserAccessScope: V Yes, can see it• The code doesn't check user's "Ask Jiminny enabled" setting• Onlv checks: tvoe = ask biminny AND arouns contains user's arounThis might be a bug - should verify if feature flag check is missingScenario D: User has automated reports only, Ask Jiminny is disabled for them• Will reports show in getPaginatedUserReports?: X No (unless filtered via teamIds )• No recipients->users match• Not Ask Jiminny. so no aroup fallbackRecommendationsFix countllserRenorts to match annlvllserAccessScone loaic.nublic function countllcerRenorts(llser Guser)• intlsgroupid = Suser-saetGrountdl).Ask anvthina (*4L)C° Adantiveio 4 spa...
|
72997
|
|
73005
|
NULL
|
0
|
2026-04-23T06:34:23.027280+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776926063027_m1.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsRepository.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
16
7
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active Ask Jiminny reports whose expiry date has passed.
*
* @return Collection<AutomatedReport>
*/
public function getExpiredActiveAskJiminnyReports(): Collection
{
return AutomatedReport::where('status', true)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereNotNull('expires_at')
->where('expires_at', '<', now()->toDateString())
->get();
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('report_id', $report->getId())
->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])
->latest()
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Restrict a query on the automated_reports table to reports the given user is allowed to see.
*
* Matches the customer-facing audience:
* - explicit user recipients (recipients.users)
* - members of any of the report's groups (Ask Jiminny reports)
*/
private function applyUserAccessScope(Builder $query, User $user): void
{
$userId = $user->getId();
$groupId = $user->getGroupId();
$query
->where('automated_reports.team_id', $user->getTeamId())
->where(function (Builder $q) use ($userId, $groupId): void {
$q->whereJsonContains('automated_reports.recipients->users', $userId);
if ($groupId !== null) {
$q->orWhere(function (Builder $sub) use ($groupId): void {
$sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereJsonContains('automated_reports.groups', $groupId);
});
}
});
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
36
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\DealInsights;
use Doctrine\DBAL\Connection;
use Generator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Jiminny\Component\DealInsights\Forecast\DealData;
use Jiminny\Component\DealInsights\Forecast\DealsFilter;
use Jiminny\Component\DealInsights\QueryBuilder\QueryBuilder;
use Jiminny\Component\DealInsights\QueryBuilder\Visitor\QueryBuilderVisitorInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Services\Crm\IntegrationApp\DTO\Utils\UrlGeneratorInterface;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class DealsRepository implements DealsRepositoryInterface
{
private Connection $connection;
private ProviderRegistry $providerRegistry;
/**
* @var QueryBuilderVisitorInterface[]
*/
private array $visitors = [];
/**
* @param QueryBuilderVisitorInterface[] $visitors
*/
public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])
{
$this->connection = $connection;
$this->providerRegistry = $crmProviderRegistry;
foreach ($visitors as $visitor) {
$this->visitors[$visitor->getIdentifier()] = $visitor;
}
}
public function getDeals(CriteriaInterface $criteria): array
{
$context = $criteria->getContext();
$team = $context->getTeam();
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$this->visit($qb, $criteria);
return $this->execute($team, $crmService, $qb);
}
public function getDeal(Team $team, int $id): array
{
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$qb->andWhere('opp.id = :id')->setParameter('id', $id);
return $this->execute($team, $crmService, $qb);
}
public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])
{
$qb = new QueryBuilder($this->connection);
$qb
->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')
->from('crm_fields', 'f')
->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')
->where('f.crm_configuration_id = :crm')
->andWhere('f.object_type = :type')
->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')
->orderBy('fd.object_id', 'ASC')
->addOrderBy('fd.updated_at', 'ASC')
->setParameter('type', Field::OBJECT_OPPORTUNITY)
->setParameter('crm', $crmId)
;
if (! empty($crmFields)) {
$fields = array_map(fn ($value): string => '"' . $value . '"', $crmFields);
$qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');
}
return $qb->executeQuery()->fetchAllAssociative();
}
public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAssociative();
}
public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('COALESCE(opp.currency_code, "' . $defaultCurrency . '") AS currency')
->addSelect('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
->groupBy('currency')
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAllAssociative();
}
public function getDealActivities(CriteriaInterface $criteria): array
{
$qb = Activity::with(['participants', 'user'])
->where('opportunity_id', $criteria->getOpportunityId())
->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())
->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())
->orderBy($criteria->getSortBy(), $criteria->getSortDirection())
;
// Should we filter activities by criteria? It's intended to filter deals.
return $qb->get()->all();
}
public function getStages(CriteriaInterface $criteria): array
{
$qb = new QueryBuilder($this->connection);
$qb
->select('id', 'label', 'sequence')
->from('stages', 's')
->where('crm_configuration_id = :crm_configuration_id')
->andWhere('type = :type')
->orderBy('sequence', 'ASC')
->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())
->setParameter('type', Stage::TYPE_OPPORTUNITY);
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$result[$row['id']] = [
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
public function getConfigurationStages(Configuration $configuration): Collection
{
return $configuration
->stages()
->where('type', Stage::TYPE_OPPORTUNITY)
->get();
}
public function getPipelineData(Configuration $crm): array
{
$qb = new QueryBuilder($this->connection);
$provider = $crm->provider;
$qb
->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')
->from('stages', 's')
->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')
->where('s.crm_configuration_id = :crm_configuration_id')
->andWhere('s.type = :type')
->orderBy('bps.business_process_id', 'ASC')
->addOrderBy('s.sequence', 'ASC')
->setParameter('crm_configuration_id', $crm->id)
->setParameter('type', Stage::TYPE_OPPORTUNITY)
;
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];
$result[$row['pipeline_id']][] = [
'value' => $value,
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
private function createQueryBuilder(string $realm): QueryBuilder
{
return (new QueryBuilder($this->connection))
->setRealm($realm)
->from('opportunities', 'opp')
->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')
->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')
->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')
;
}
/**
* Applies all applicable visitors and returns the IDs of the executed ones
*
* @return string[]
*/
private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array
{
$queryVisitors = [];
foreach ($this->visitors as $visitor) {
if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {
$visitor->visit($queryBuilder, $criteria);
$queryVisitors[] = $visitor->getIdentifier();
}
}
return $queryVisitors;
}
private function hydrateStages(array $deals): array
{
foreach ($this->fetchStages(array_keys($deals)) as $stage) {
$oppId = (int) $stage['opportunity_id'];
if (! isset($deals[$oppId])) {
continue; // or throw??!
}
$deals[$oppId]['stages'][] = [
'id' => $stage['stage_id'],
'name' => $stage['label'],
'enteredAt' => $stage['created_at'],
];
}
return $deals;
}
/**
* @param int[] $dealIds
*/
private function fetchStages(array $dealIds): array
{
if (empty($dealIds)) {
return [];
}
$qb = new QueryBuilder($this->connection);
$qb
->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')
->from('opportunity_stages', 'os')
->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')
->where($qb->expr()->in('os.opportunity_id', $dealIds))
->orderBy('os.opportunity_id', 'ASC')
->addOrderBy('s.created_at', 'ASC')
;
return $qb->executeQuery()->fetchAllAssociative();
}
private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array
{
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$data = [
'uuid' => RequiresUUID::toNormal($row['uuid']),
'name' => $row['name'],
'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),
'account' => [
'name' => $row['acc_name'],
'url' => $crmService->generateProviderUrl(
providerId: $row['acc_provider_id'],
objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'
),
],
'owner' => null,
'rawValue' => [
'amount' => (float) $row['value'],
'currency' => $row['currency_code'],
],
'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),
'openDate' => $row['remotely_created_at'] ?? null,
'closeDate' => $row['close_date'] ?? null,
'stages' => [],
'currentPipelineId' => $row['pipeline_id'],
'currentStage' => [
'id' => $row['stage_id'],
'enteredAt' => $row['stage_updated_at'],
],
'currentStageUpdatedAt' => $row['stage_updated_at'],
'isClosed' => (bool) $row['is_closed'],
'isWon' => (bool) $row['is_won'],
];
if (isset($row['owner_uuid'])) {
$data['owner'] = [
'uuid' => RequiresUUID::toNormal($row['owner_uuid']),
'name' => $row['owner_name'],
'photoUrl' => $row['owner_photo'] === null
? null
: client_cdn($row['owner_photo'], $team),
'id' => $row['owner_id'],
'job' => $row['owner_job'],
];
}
$result[(int) $row['opp_id']] = $data;
}
return $this->hydrateStages($result);
}
private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder
{
$qb = clone $queryBuilder;
$qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');
$qb
->select(...[
'opp.id as opp_id',
'opp.uuid',
'opp.name',
'opp.value',
'opp.currency_code',
'opp.close_date',
'opp.remotely_created_at',
'opp.is_closed',
'opp.is_won',
])
->addSelect(...[
'usr.uuid as owner_uuid',
'usr.name AS owner_name',
'usr.photo_path as owner_photo',
'usr.id AS owner_id',
'jt.name as owner_job',
])
->addSelect('opp.stage_id', 'opp.stage_updated_at')
->addSelect(...[
'acc.name AS acc_name',
'acc.is_internal as acc_is_internal',
'opp.stage_updated_at',
'acc.crm_provider_id AS acc_provider_id',
'opp.crm_provider_id AS opp_provider_id',
])
->addSelect('rt.business_process_id AS pipeline_id')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'));
return $qb;
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws SocialAccountTokenInvalidException
*/
private function getCrmService(Team $team): ServiceInterface
{
$crmService = $this->providerRegistry->get($team->crm->provider);
$crmService->setConfiguration($team->crm);
if ($crmService instanceof UrlGeneratorInterface) {
$crmService->setCrmUrlGenerator($team->crm);
}
return $crmService;
}
/**
*
* @return Generator<DealData>
*/
public function getForecastData(DealsFilter $filter): Generator
{
$opportunities = DB::query()
->select([
'o.value',
'o.close_date',
'o.currency_code',
'o.is_won',
'o.is_closed',
'o.probability',
'o.forecast_category',
])
->from('opportunities', 'o')
->join('users', 'users.id', '=', 'o.user_id')
->join('groups', 'groups.id', '=', 'users.group_id')
->where('users.team_id', $filter->getTeam()->getId())
->where('o.close_date', '>=', $filter->getStartDate())
->where('o.close_date', '<=', $filter->getEndDate())
->where('o.currency_code', $filter->getCurrency())
->where('o.deleted_at', '=', null)
;
$userUuidList = $filter->getUserUuidList();
if (! empty($userUuidList)) {
$userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);
$opportunities->whereIn('users.uuid', $userUuidList);
}
$groupUuidList = $filter->getGroupUuidList();
if (! empty($groupUuidList)) {
$groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);
$opportunities->whereIn('groups.uuid', $groupUuidList);
}
foreach ($opportunities->cursor() as $row) {
yield new DealData(
(float) $row->value,
$row->close_date,
! empty($row->is_won),
! empty($row->is_closed),
$row->probability ?: 0,
$row->forecast_category ?: '',
);
}
}
public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection
{
return $user->subscriptionSets()
->where(static function (Eloquent\Builder $query): void {
$query
->whereNull('expired_at')
->orWhere('expired_at', '>=', now());
})
->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {
$join
->on('subscription_set_id', '=', 'activity_subscription_sets.id');
$join
->where('followable_type', Models\Activity\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)
->whereIn('followable_id', $opportunityIds);
})
->pluck('followable_id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20157-AJ-report-not-send-notification, menu","depth":5,"help_text":"Git Branch: JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"RequestGenerateAskJiminnyReportJobTest","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"16","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"7","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active Ask Jiminny reports whose expiry date has passed.\n *\n * @return Collection<AutomatedReport>\n */\n public function getExpiredActiveAskJiminnyReports(): Collection\n {\n return AutomatedReport::where('status', true)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereNotNull('expires_at')\n ->where('expires_at', '<', now()->toDateString())\n ->get();\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('report_id', $report->getId())\n ->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])\n ->latest()\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Restrict a query on the automated_reports table to reports the given user is allowed to see.\n *\n * Matches the customer-facing audience:\n * - explicit user recipients (recipients.users)\n * - members of any of the report's groups (Ask Jiminny reports)\n */\n private function applyUserAccessScope(Builder $query, User $user): void\n {\n $userId = $user->getId();\n $groupId = $user->getGroupId();\n\n $query\n ->where('automated_reports.team_id', $user->getTeamId())\n ->where(function (Builder $q) use ($userId, $groupId): void {\n $q->whereJsonContains('automated_reports.recipients->users', $userId);\n\n if ($groupId !== null) {\n $q->orWhere(function (Builder $sub) use ($groupId): void {\n $sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereJsonContains('automated_reports.groups', $groupId);\n });\n }\n });\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active Ask Jiminny reports whose expiry date has passed.\n *\n * @return Collection<AutomatedReport>\n */\n public function getExpiredActiveAskJiminnyReports(): Collection\n {\n return AutomatedReport::where('status', true)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereNotNull('expires_at')\n ->where('expires_at', '<', now()->toDateString())\n ->get();\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('report_id', $report->getId())\n ->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])\n ->latest()\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Restrict a query on the automated_reports table to reports the given user is allowed to see.\n *\n * Matches the customer-facing audience:\n * - explicit user recipients (recipients.users)\n * - members of any of the report's groups (Ask Jiminny reports)\n */\n private function applyUserAccessScope(Builder $query, User $user): void\n {\n $userId = $user->getId();\n $groupId = $user->getGroupId();\n\n $query\n ->where('automated_reports.team_id', $user->getTeamId())\n ->where(function (Builder $q) use ($userId, $groupId): void {\n $q->whereJsonContains('automated_reports.recipients->users', $userId);\n\n if ($groupId !== null) {\n $q->orWhere(function (Builder $sub) use ($groupId): void {\n $sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereJsonContains('automated_reports.groups', $groupId);\n });\n }\n });\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"36","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\DealInsights;\n\nuse Doctrine\\DBAL\\Connection;\nuse Generator;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealData;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealsFilter;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\QueryBuilder;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\Visitor\\QueryBuilderVisitorInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Services\\Crm\\IntegrationApp\\DTO\\Utils\\UrlGeneratorInterface;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Eloquent;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass DealsRepository implements DealsRepositoryInterface\n{\n private Connection $connection;\n\n private ProviderRegistry $providerRegistry;\n\n /**\n * @var QueryBuilderVisitorInterface[]\n */\n private array $visitors = [];\n\n /**\n * @param QueryBuilderVisitorInterface[] $visitors\n */\n public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])\n {\n $this->connection = $connection;\n $this->providerRegistry = $crmProviderRegistry;\n\n foreach ($visitors as $visitor) {\n $this->visitors[$visitor->getIdentifier()] = $visitor;\n }\n }\n\n public function getDeals(CriteriaInterface $criteria): array\n {\n $context = $criteria->getContext();\n $team = $context->getTeam();\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n\n $this->visit($qb, $criteria);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getDeal(Team $team, int $id): array\n {\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n $qb->andWhere('opp.id = :id')->setParameter('id', $id);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')\n ->from('crm_fields', 'f')\n ->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')\n ->where('f.crm_configuration_id = :crm')\n ->andWhere('f.object_type = :type')\n ->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')\n ->orderBy('fd.object_id', 'ASC')\n ->addOrderBy('fd.updated_at', 'ASC')\n\n ->setParameter('type', Field::OBJECT_OPPORTUNITY)\n ->setParameter('crm', $crmId)\n ;\n\n if (! empty($crmFields)) {\n $fields = array_map(fn ($value): string => '\"' . $value . '\"', $crmFields);\n $qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');\n }\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAssociative();\n }\n\n public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('COALESCE(opp.currency_code, \"' . $defaultCurrency . '\") AS currency')\n ->addSelect('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ->groupBy('currency')\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getDealActivities(CriteriaInterface $criteria): array\n {\n $qb = Activity::with(['participants', 'user'])\n ->where('opportunity_id', $criteria->getOpportunityId())\n ->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())\n ->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())\n ->orderBy($criteria->getSortBy(), $criteria->getSortDirection())\n ;\n\n // Should we filter activities by criteria? It's intended to filter deals.\n\n return $qb->get()->all();\n }\n\n public function getStages(CriteriaInterface $criteria): array\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('id', 'label', 'sequence')\n ->from('stages', 's')\n ->where('crm_configuration_id = :crm_configuration_id')\n ->andWhere('type = :type')\n ->orderBy('sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())\n ->setParameter('type', Stage::TYPE_OPPORTUNITY);\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $result[$row['id']] = [\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n public function getConfigurationStages(Configuration $configuration): Collection\n {\n return $configuration\n ->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get();\n }\n\n public function getPipelineData(Configuration $crm): array\n {\n $qb = new QueryBuilder($this->connection);\n $provider = $crm->provider;\n\n $qb\n ->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')\n ->from('stages', 's')\n ->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')\n ->where('s.crm_configuration_id = :crm_configuration_id')\n ->andWhere('s.type = :type')\n ->orderBy('bps.business_process_id', 'ASC')\n ->addOrderBy('s.sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $crm->id)\n ->setParameter('type', Stage::TYPE_OPPORTUNITY)\n ;\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];\n $result[$row['pipeline_id']][] = [\n 'value' => $value,\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n private function createQueryBuilder(string $realm): QueryBuilder\n {\n return (new QueryBuilder($this->connection))\n ->setRealm($realm)\n ->from('opportunities', 'opp')\n ->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')\n ->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')\n ->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')\n ;\n }\n\n /**\n * Applies all applicable visitors and returns the IDs of the executed ones\n *\n * @return string[]\n */\n private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array\n {\n $queryVisitors = [];\n\n foreach ($this->visitors as $visitor) {\n if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {\n $visitor->visit($queryBuilder, $criteria);\n\n $queryVisitors[] = $visitor->getIdentifier();\n }\n }\n\n return $queryVisitors;\n }\n\n private function hydrateStages(array $deals): array\n {\n foreach ($this->fetchStages(array_keys($deals)) as $stage) {\n $oppId = (int) $stage['opportunity_id'];\n\n if (! isset($deals[$oppId])) {\n continue; // or throw??!\n }\n\n $deals[$oppId]['stages'][] = [\n 'id' => $stage['stage_id'],\n 'name' => $stage['label'],\n 'enteredAt' => $stage['created_at'],\n ];\n }\n\n return $deals;\n }\n\n /**\n * @param int[] $dealIds\n */\n private function fetchStages(array $dealIds): array\n {\n if (empty($dealIds)) {\n return [];\n }\n\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')\n ->from('opportunity_stages', 'os')\n ->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')\n ->where($qb->expr()->in('os.opportunity_id', $dealIds))\n ->orderBy('os.opportunity_id', 'ASC')\n ->addOrderBy('s.created_at', 'ASC')\n ;\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array\n {\n $result = [];\n\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $data = [\n 'uuid' => RequiresUUID::toNormal($row['uuid']),\n 'name' => $row['name'],\n 'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),\n 'account' => [\n 'name' => $row['acc_name'],\n 'url' => $crmService->generateProviderUrl(\n providerId: $row['acc_provider_id'],\n objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'\n ),\n ],\n 'owner' => null,\n 'rawValue' => [\n 'amount' => (float) $row['value'],\n 'currency' => $row['currency_code'],\n ],\n 'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),\n 'openDate' => $row['remotely_created_at'] ?? null,\n 'closeDate' => $row['close_date'] ?? null,\n 'stages' => [],\n 'currentPipelineId' => $row['pipeline_id'],\n 'currentStage' => [\n 'id' => $row['stage_id'],\n 'enteredAt' => $row['stage_updated_at'],\n ],\n 'currentStageUpdatedAt' => $row['stage_updated_at'],\n 'isClosed' => (bool) $row['is_closed'],\n 'isWon' => (bool) $row['is_won'],\n ];\n\n if (isset($row['owner_uuid'])) {\n $data['owner'] = [\n 'uuid' => RequiresUUID::toNormal($row['owner_uuid']),\n 'name' => $row['owner_name'],\n 'photoUrl' => $row['owner_photo'] === null\n ? null\n : client_cdn($row['owner_photo'], $team),\n 'id' => $row['owner_id'],\n 'job' => $row['owner_job'],\n ];\n }\n\n $result[(int) $row['opp_id']] = $data;\n }\n\n return $this->hydrateStages($result);\n }\n\n private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder\n {\n $qb = clone $queryBuilder;\n $qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');\n\n $qb\n ->select(...[\n 'opp.id as opp_id',\n 'opp.uuid',\n 'opp.name',\n 'opp.value',\n 'opp.currency_code',\n 'opp.close_date',\n 'opp.remotely_created_at',\n 'opp.is_closed',\n 'opp.is_won',\n ])\n ->addSelect(...[\n 'usr.uuid as owner_uuid',\n 'usr.name AS owner_name',\n 'usr.photo_path as owner_photo',\n 'usr.id AS owner_id',\n 'jt.name as owner_job',\n ])\n ->addSelect('opp.stage_id', 'opp.stage_updated_at')\n ->addSelect(...[\n 'acc.name AS acc_name',\n 'acc.is_internal as acc_is_internal',\n 'opp.stage_updated_at',\n 'acc.crm_provider_id AS acc_provider_id',\n 'opp.crm_provider_id AS opp_provider_id',\n ])\n ->addSelect('rt.business_process_id AS pipeline_id')\n\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'));\n\n return $qb;\n }\n\n /**\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws SocialAccountTokenInvalidException\n */\n private function getCrmService(Team $team): ServiceInterface\n {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n $crmService->setConfiguration($team->crm);\n if ($crmService instanceof UrlGeneratorInterface) {\n $crmService->setCrmUrlGenerator($team->crm);\n }\n\n return $crmService;\n }\n\n /**\n *\n * @return Generator<DealData>\n */\n public function getForecastData(DealsFilter $filter): Generator\n {\n $opportunities = DB::query()\n ->select([\n 'o.value',\n 'o.close_date',\n 'o.currency_code',\n 'o.is_won',\n 'o.is_closed',\n 'o.probability',\n 'o.forecast_category',\n ])\n ->from('opportunities', 'o')\n ->join('users', 'users.id', '=', 'o.user_id')\n ->join('groups', 'groups.id', '=', 'users.group_id')\n ->where('users.team_id', $filter->getTeam()->getId())\n ->where('o.close_date', '>=', $filter->getStartDate())\n ->where('o.close_date', '<=', $filter->getEndDate())\n ->where('o.currency_code', $filter->getCurrency())\n ->where('o.deleted_at', '=', null)\n ;\n\n $userUuidList = $filter->getUserUuidList();\n if (! empty($userUuidList)) {\n $userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);\n\n $opportunities->whereIn('users.uuid', $userUuidList);\n }\n\n $groupUuidList = $filter->getGroupUuidList();\n if (! empty($groupUuidList)) {\n $groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);\n\n $opportunities->whereIn('groups.uuid', $groupUuidList);\n }\n\n foreach ($opportunities->cursor() as $row) {\n yield new DealData(\n (float) $row->value,\n $row->close_date,\n ! empty($row->is_won),\n ! empty($row->is_closed),\n $row->probability ?: 0,\n $row->forecast_category ?: '',\n );\n }\n }\n\n public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection\n {\n return $user->subscriptionSets()\n ->where(static function (Eloquent\\Builder $query): void {\n $query\n ->whereNull('expired_at')\n ->orWhere('expired_at', '>=', now());\n })\n ->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n $join\n ->where('followable_type', Models\\Activity\\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)\n ->whereIn('followable_id', $opportunityIds);\n })\n ->pluck('followable_id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\DealInsights;\n\nuse Doctrine\\DBAL\\Connection;\nuse Generator;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealData;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealsFilter;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\QueryBuilder;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\Visitor\\QueryBuilderVisitorInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Services\\Crm\\IntegrationApp\\DTO\\Utils\\UrlGeneratorInterface;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Eloquent;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass DealsRepository implements DealsRepositoryInterface\n{\n private Connection $connection;\n\n private ProviderRegistry $providerRegistry;\n\n /**\n * @var QueryBuilderVisitorInterface[]\n */\n private array $visitors = [];\n\n /**\n * @param QueryBuilderVisitorInterface[] $visitors\n */\n public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])\n {\n $this->connection = $connection;\n $this->providerRegistry = $crmProviderRegistry;\n\n foreach ($visitors as $visitor) {\n $this->visitors[$visitor->getIdentifier()] = $visitor;\n }\n }\n\n public function getDeals(CriteriaInterface $criteria): array\n {\n $context = $criteria->getContext();\n $team = $context->getTeam();\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n\n $this->visit($qb, $criteria);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getDeal(Team $team, int $id): array\n {\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n $qb->andWhere('opp.id = :id')->setParameter('id', $id);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')\n ->from('crm_fields', 'f')\n ->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')\n ->where('f.crm_configuration_id = :crm')\n ->andWhere('f.object_type = :type')\n ->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')\n ->orderBy('fd.object_id', 'ASC')\n ->addOrderBy('fd.updated_at', 'ASC')\n\n ->setParameter('type', Field::OBJECT_OPPORTUNITY)\n ->setParameter('crm', $crmId)\n ;\n\n if (! empty($crmFields)) {\n $fields = array_map(fn ($value): string => '\"' . $value . '\"', $crmFields);\n $qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');\n }\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAssociative();\n }\n\n public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('COALESCE(opp.currency_code, \"' . $defaultCurrency . '\") AS currency')\n ->addSelect('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ->groupBy('currency')\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getDealActivities(CriteriaInterface $criteria): array\n {\n $qb = Activity::with(['participants', 'user'])\n ->where('opportunity_id', $criteria->getOpportunityId())\n ->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())\n ->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())\n ->orderBy($criteria->getSortBy(), $criteria->getSortDirection())\n ;\n\n // Should we filter activities by criteria? It's intended to filter deals.\n\n return $qb->get()->all();\n }\n\n public function getStages(CriteriaInterface $criteria): array\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('id', 'label', 'sequence')\n ->from('stages', 's')\n ->where('crm_configuration_id = :crm_configuration_id')\n ->andWhere('type = :type')\n ->orderBy('sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())\n ->setParameter('type', Stage::TYPE_OPPORTUNITY);\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $result[$row['id']] = [\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n public function getConfigurationStages(Configuration $configuration): Collection\n {\n return $configuration\n ->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get();\n }\n\n public function getPipelineData(Configuration $crm): array\n {\n $qb = new QueryBuilder($this->connection);\n $provider = $crm->provider;\n\n $qb\n ->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')\n ->from('stages', 's')\n ->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')\n ->where('s.crm_configuration_id = :crm_configuration_id')\n ->andWhere('s.type = :type')\n ->orderBy('bps.business_process_id', 'ASC')\n ->addOrderBy('s.sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $crm->id)\n ->setParameter('type', Stage::TYPE_OPPORTUNITY)\n ;\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];\n $result[$row['pipeline_id']][] = [\n 'value' => $value,\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n private function createQueryBuilder(string $realm): QueryBuilder\n {\n return (new QueryBuilder($this->connection))\n ->setRealm($realm)\n ->from('opportunities', 'opp')\n ->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')\n ->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')\n ->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')\n ;\n }\n\n /**\n * Applies all applicable visitors and returns the IDs of the executed ones\n *\n * @return string[]\n */\n private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array\n {\n $queryVisitors = [];\n\n foreach ($this->visitors as $visitor) {\n if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {\n $visitor->visit($queryBuilder, $criteria);\n\n $queryVisitors[] = $visitor->getIdentifier();\n }\n }\n\n return $queryVisitors;\n }\n\n private function hydrateStages(array $deals): array\n {\n foreach ($this->fetchStages(array_keys($deals)) as $stage) {\n $oppId = (int) $stage['opportunity_id'];\n\n if (! isset($deals[$oppId])) {\n continue; // or throw??!\n }\n\n $deals[$oppId]['stages'][] = [\n 'id' => $stage['stage_id'],\n 'name' => $stage['label'],\n 'enteredAt' => $stage['created_at'],\n ];\n }\n\n return $deals;\n }\n\n /**\n * @param int[] $dealIds\n */\n private function fetchStages(array $dealIds): array\n {\n if (empty($dealIds)) {\n return [];\n }\n\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')\n ->from('opportunity_stages', 'os')\n ->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')\n ->where($qb->expr()->in('os.opportunity_id', $dealIds))\n ->orderBy('os.opportunity_id', 'ASC')\n ->addOrderBy('s.created_at', 'ASC')\n ;\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array\n {\n $result = [];\n\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $data = [\n 'uuid' => RequiresUUID::toNormal($row['uuid']),\n 'name' => $row['name'],\n 'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),\n 'account' => [\n 'name' => $row['acc_name'],\n 'url' => $crmService->generateProviderUrl(\n providerId: $row['acc_provider_id'],\n objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'\n ),\n ],\n 'owner' => null,\n 'rawValue' => [\n 'amount' => (float) $row['value'],\n 'currency' => $row['currency_code'],\n ],\n 'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),\n 'openDate' => $row['remotely_created_at'] ?? null,\n 'closeDate' => $row['close_date'] ?? null,\n 'stages' => [],\n 'currentPipelineId' => $row['pipeline_id'],\n 'currentStage' => [\n 'id' => $row['stage_id'],\n 'enteredAt' => $row['stage_updated_at'],\n ],\n 'currentStageUpdatedAt' => $row['stage_updated_at'],\n 'isClosed' => (bool) $row['is_closed'],\n 'isWon' => (bool) $row['is_won'],\n ];\n\n if (isset($row['owner_uuid'])) {\n $data['owner'] = [\n 'uuid' => RequiresUUID::toNormal($row['owner_uuid']),\n 'name' => $row['owner_name'],\n 'photoUrl' => $row['owner_photo'] === null\n ? null\n : client_cdn($row['owner_photo'], $team),\n 'id' => $row['owner_id'],\n 'job' => $row['owner_job'],\n ];\n }\n\n $result[(int) $row['opp_id']] = $data;\n }\n\n return $this->hydrateStages($result);\n }\n\n private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder\n {\n $qb = clone $queryBuilder;\n $qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');\n\n $qb\n ->select(...[\n 'opp.id as opp_id',\n 'opp.uuid',\n 'opp.name',\n 'opp.value',\n 'opp.currency_code',\n 'opp.close_date',\n 'opp.remotely_created_at',\n 'opp.is_closed',\n 'opp.is_won',\n ])\n ->addSelect(...[\n 'usr.uuid as owner_uuid',\n 'usr.name AS owner_name',\n 'usr.photo_path as owner_photo',\n 'usr.id AS owner_id',\n 'jt.name as owner_job',\n ])\n ->addSelect('opp.stage_id', 'opp.stage_updated_at')\n ->addSelect(...[\n 'acc.name AS acc_name',\n 'acc.is_internal as acc_is_internal',\n 'opp.stage_updated_at',\n 'acc.crm_provider_id AS acc_provider_id',\n 'opp.crm_provider_id AS opp_provider_id',\n ])\n ->addSelect('rt.business_process_id AS pipeline_id')\n\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'));\n\n return $qb;\n }\n\n /**\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws SocialAccountTokenInvalidException\n */\n private function getCrmService(Team $team): ServiceInterface\n {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n $crmService->setConfiguration($team->crm);\n if ($crmService instanceof UrlGeneratorInterface) {\n $crmService->setCrmUrlGenerator($team->crm);\n }\n\n return $crmService;\n }\n\n /**\n *\n * @return Generator<DealData>\n */\n public function getForecastData(DealsFilter $filter): Generator\n {\n $opportunities = DB::query()\n ->select([\n 'o.value',\n 'o.close_date',\n 'o.currency_code',\n 'o.is_won',\n 'o.is_closed',\n 'o.probability',\n 'o.forecast_category',\n ])\n ->from('opportunities', 'o')\n ->join('users', 'users.id', '=', 'o.user_id')\n ->join('groups', 'groups.id', '=', 'users.group_id')\n ->where('users.team_id', $filter->getTeam()->getId())\n ->where('o.close_date', '>=', $filter->getStartDate())\n ->where('o.close_date', '<=', $filter->getEndDate())\n ->where('o.currency_code', $filter->getCurrency())\n ->where('o.deleted_at', '=', null)\n ;\n\n $userUuidList = $filter->getUserUuidList();\n if (! empty($userUuidList)) {\n $userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);\n\n $opportunities->whereIn('users.uuid', $userUuidList);\n }\n\n $groupUuidList = $filter->getGroupUuidList();\n if (! empty($groupUuidList)) {\n $groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);\n\n $opportunities->whereIn('groups.uuid', $groupUuidList);\n }\n\n foreach ($opportunities->cursor() as $row) {\n yield new DealData(\n (float) $row->value,\n $row->close_date,\n ! empty($row->is_won),\n ! empty($row->is_closed),\n $row->probability ?: 0,\n $row->forecast_category ?: '',\n );\n }\n }\n\n public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection\n {\n return $user->subscriptionSets()\n ->where(static function (Eloquent\\Builder $query): void {\n $query\n ->whereNull('expired_at')\n ->orWhere('expired_at', '>=', now());\n })\n ->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n $join\n ->where('followable_type', Models\\Activity\\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)\n ->whereIn('followable_id', $opportunityIds);\n })\n ->pluck('followable_id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3407362332434637772
|
-4167466460282672524
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
16
7
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active Ask Jiminny reports whose expiry date has passed.
*
* @return Collection<AutomatedReport>
*/
public function getExpiredActiveAskJiminnyReports(): Collection
{
return AutomatedReport::where('status', true)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereNotNull('expires_at')
->where('expires_at', '<', now()->toDateString())
->get();
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('report_id', $report->getId())
->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])
->latest()
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Restrict a query on the automated_reports table to reports the given user is allowed to see.
*
* Matches the customer-facing audience:
* - explicit user recipients (recipients.users)
* - members of any of the report's groups (Ask Jiminny reports)
*/
private function applyUserAccessScope(Builder $query, User $user): void
{
$userId = $user->getId();
$groupId = $user->getGroupId();
$query
->where('automated_reports.team_id', $user->getTeamId())
->where(function (Builder $q) use ($userId, $groupId): void {
$q->whereJsonContains('automated_reports.recipients->users', $userId);
if ($groupId !== null) {
$q->orWhere(function (Builder $sub) use ($groupId): void {
$sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereJsonContains('automated_reports.groups', $groupId);
});
}
});
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
36
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\DealInsights;
use Doctrine\DBAL\Connection;
use Generator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Jiminny\Component\DealInsights\Forecast\DealData;
use Jiminny\Component\DealInsights\Forecast\DealsFilter;
use Jiminny\Component\DealInsights\QueryBuilder\QueryBuilder;
use Jiminny\Component\DealInsights\QueryBuilder\Visitor\QueryBuilderVisitorInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Services\Crm\IntegrationApp\DTO\Utils\UrlGeneratorInterface;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class DealsRepository implements DealsRepositoryInterface
{
private Connection $connection;
private ProviderRegistry $providerRegistry;
/**
* @var QueryBuilderVisitorInterface[]
*/
private array $visitors = [];
/**
* @param QueryBuilderVisitorInterface[] $visitors
*/
public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])
{
$this->connection = $connection;
$this->providerRegistry = $crmProviderRegistry;
foreach ($visitors as $visitor) {
$this->visitors[$visitor->getIdentifier()] = $visitor;
}
}
public function getDeals(CriteriaInterface $criteria): array
{
$context = $criteria->getContext();
$team = $context->getTeam();
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$this->visit($qb, $criteria);
return $this->execute($team, $crmService, $qb);
}
public function getDeal(Team $team, int $id): array
{
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$qb->andWhere('opp.id = :id')->setParameter('id', $id);
return $this->execute($team, $crmService, $qb);
}
public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])
{
$qb = new QueryBuilder($this->connection);
$qb
->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')
->from('crm_fields', 'f')
->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')
->where('f.crm_configuration_id = :crm')
->andWhere('f.object_type = :type')
->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')
->orderBy('fd.object_id', 'ASC')
->addOrderBy('fd.updated_at', 'ASC')
->setParameter('type', Field::OBJECT_OPPORTUNITY)
->setParameter('crm', $crmId)
;
if (! empty($crmFields)) {
$fields = array_map(fn ($value): string => '"' . $value . '"', $crmFields);
$qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');
}
return $qb->executeQuery()->fetchAllAssociative();
}
public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAssociative();
}
public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('COALESCE(opp.currency_code, "' . $defaultCurrency . '") AS currency')
->addSelect('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
->groupBy('currency')
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAllAssociative();
}
public function getDealActivities(CriteriaInterface $criteria): array
{
$qb = Activity::with(['participants', 'user'])
->where('opportunity_id', $criteria->getOpportunityId())
->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())
->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())
->orderBy($criteria->getSortBy(), $criteria->getSortDirection())
;
// Should we filter activities by criteria? It's intended to filter deals.
return $qb->get()->all();
}
public function getStages(CriteriaInterface $criteria): array
{
$qb = new QueryBuilder($this->connection);
$qb
->select('id', 'label', 'sequence')
->from('stages', 's')
->where('crm_configuration_id = :crm_configuration_id')
->andWhere('type = :type')
->orderBy('sequence', 'ASC')
->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())
->setParameter('type', Stage::TYPE_OPPORTUNITY);
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$result[$row['id']] = [
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
public function getConfigurationStages(Configuration $configuration): Collection
{
return $configuration
->stages()
->where('type', Stage::TYPE_OPPORTUNITY)
->get();
}
public function getPipelineData(Configuration $crm): array
{
$qb = new QueryBuilder($this->connection);
$provider = $crm->provider;
$qb
->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')
->from('stages', 's')
->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')
->where('s.crm_configuration_id = :crm_configuration_id')
->andWhere('s.type = :type')
->orderBy('bps.business_process_id', 'ASC')
->addOrderBy('s.sequence', 'ASC')
->setParameter('crm_configuration_id', $crm->id)
->setParameter('type', Stage::TYPE_OPPORTUNITY)
;
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];
$result[$row['pipeline_id']][] = [
'value' => $value,
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
private function createQueryBuilder(string $realm): QueryBuilder
{
return (new QueryBuilder($this->connection))
->setRealm($realm)
->from('opportunities', 'opp')
->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')
->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')
->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')
;
}
/**
* Applies all applicable visitors and returns the IDs of the executed ones
*
* @return string[]
*/
private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array
{
$queryVisitors = [];
foreach ($this->visitors as $visitor) {
if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {
$visitor->visit($queryBuilder, $criteria);
$queryVisitors[] = $visitor->getIdentifier();
}
}
return $queryVisitors;
}
private function hydrateStages(array $deals): array
{
foreach ($this->fetchStages(array_keys($deals)) as $stage) {
$oppId = (int) $stage['opportunity_id'];
if (! isset($deals[$oppId])) {
continue; // or throw??!
}
$deals[$oppId]['stages'][] = [
'id' => $stage['stage_id'],
'name' => $stage['label'],
'enteredAt' => $stage['created_at'],
];
}
return $deals;
}
/**
* @param int[] $dealIds
*/
private function fetchStages(array $dealIds): array
{
if (empty($dealIds)) {
return [];
}
$qb = new QueryBuilder($this->connection);
$qb
->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')
->from('opportunity_stages', 'os')
->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')
->where($qb->expr()->in('os.opportunity_id', $dealIds))
->orderBy('os.opportunity_id', 'ASC')
->addOrderBy('s.created_at', 'ASC')
;
return $qb->executeQuery()->fetchAllAssociative();
}
private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array
{
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$data = [
'uuid' => RequiresUUID::toNormal($row['uuid']),
'name' => $row['name'],
'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),
'account' => [
'name' => $row['acc_name'],
'url' => $crmService->generateProviderUrl(
providerId: $row['acc_provider_id'],
objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'
),
],
'owner' => null,
'rawValue' => [
'amount' => (float) $row['value'],
'currency' => $row['currency_code'],
],
'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),
'openDate' => $row['remotely_created_at'] ?? null,
'closeDate' => $row['close_date'] ?? null,
'stages' => [],
'currentPipelineId' => $row['pipeline_id'],
'currentStage' => [
'id' => $row['stage_id'],
'enteredAt' => $row['stage_updated_at'],
],
'currentStageUpdatedAt' => $row['stage_updated_at'],
'isClosed' => (bool) $row['is_closed'],
'isWon' => (bool) $row['is_won'],
];
if (isset($row['owner_uuid'])) {
$data['owner'] = [
'uuid' => RequiresUUID::toNormal($row['owner_uuid']),
'name' => $row['owner_name'],
'photoUrl' => $row['owner_photo'] === null
? null
: client_cdn($row['owner_photo'], $team),
'id' => $row['owner_id'],
'job' => $row['owner_job'],
];
}
$result[(int) $row['opp_id']] = $data;
}
return $this->hydrateStages($result);
}
private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder
{
$qb = clone $queryBuilder;
$qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');
$qb
->select(...[
'opp.id as opp_id',
'opp.uuid',
'opp.name',
'opp.value',
'opp.currency_code',
'opp.close_date',
'opp.remotely_created_at',
'opp.is_closed',
'opp.is_won',
])
->addSelect(...[
'usr.uuid as owner_uuid',
'usr.name AS owner_name',
'usr.photo_path as owner_photo',
'usr.id AS owner_id',
'jt.name as owner_job',
])
->addSelect('opp.stage_id', 'opp.stage_updated_at')
->addSelect(...[
'acc.name AS acc_name',
'acc.is_internal as acc_is_internal',
'opp.stage_updated_at',
'acc.crm_provider_id AS acc_provider_id',
'opp.crm_provider_id AS opp_provider_id',
])
->addSelect('rt.business_process_id AS pipeline_id')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'));
return $qb;
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws SocialAccountTokenInvalidException
*/
private function getCrmService(Team $team): ServiceInterface
{
$crmService = $this->providerRegistry->get($team->crm->provider);
$crmService->setConfiguration($team->crm);
if ($crmService instanceof UrlGeneratorInterface) {
$crmService->setCrmUrlGenerator($team->crm);
}
return $crmService;
}
/**
*
* @return Generator<DealData>
*/
public function getForecastData(DealsFilter $filter): Generator
{
$opportunities = DB::query()
->select([
'o.value',
'o.close_date',
'o.currency_code',
'o.is_won',
'o.is_closed',
'o.probability',
'o.forecast_category',
])
->from('opportunities', 'o')
->join('users', 'users.id', '=', 'o.user_id')
->join('groups', 'groups.id', '=', 'users.group_id')
->where('users.team_id', $filter->getTeam()->getId())
->where('o.close_date', '>=', $filter->getStartDate())
->where('o.close_date', '<=', $filter->getEndDate())
->where('o.currency_code', $filter->getCurrency())
->where('o.deleted_at', '=', null)
;
$userUuidList = $filter->getUserUuidList();
if (! empty($userUuidList)) {
$userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);
$opportunities->whereIn('users.uuid', $userUuidList);
}
$groupUuidList = $filter->getGroupUuidList();
if (! empty($groupUuidList)) {
$groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);
$opportunities->whereIn('groups.uuid', $groupUuidList);
}
foreach ($opportunities->cursor() as $row) {
yield new DealData(
(float) $row->value,
$row->close_date,
! empty($row->is_won),
! empty($row->is_closed),
$row->probability ?: 0,
$row->forecast_category ?: '',
);
}
}
public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection
{
return $user->subscriptionSets()
->where(static function (Eloquent\Builder $query): void {
$query
->whereNull('expired_at')
->orWhere('expired_at', '>=', now());
})
->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {
$join
->on('subscription_set_id', '=', 'activity_subscription_sets.id');
$join
->where('followable_type', Models\Activity\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)
->whereIn('followable_id', $opportunityIds);
})
->pluck('followable_id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
72999
|
|
73004
|
NULL
|
0
|
2026-04-23T06:34:22.442985+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776926062442_m2.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsRepository.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
16
7
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active Ask Jiminny reports whose expiry date has passed.
*
* @return Collection<AutomatedReport>
*/
public function getExpiredActiveAskJiminnyReports(): Collection
{
return AutomatedReport::where('status', true)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereNotNull('expires_at')
->where('expires_at', '<', now()->toDateString())
->get();
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('report_id', $report->getId())
->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])
->latest()
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Restrict a query on the automated_reports table to reports the given user is allowed to see.
*
* Matches the customer-facing audience:
* - explicit user recipients (recipients.users)
* - members of any of the report's groups (Ask Jiminny reports)
*/
private function applyUserAccessScope(Builder $query, User $user): void
{
$userId = $user->getId();
$groupId = $user->getGroupId();
$query
->where('automated_reports.team_id', $user->getTeamId())
->where(function (Builder $q) use ($userId, $groupId): void {
$q->whereJsonContains('automated_reports.recipients->users', $userId);
if ($groupId !== null) {
$q->orWhere(function (Builder $sub) use ($groupId): void {
$sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereJsonContains('automated_reports.groups', $groupId);
});
}
});
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
36
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\DealInsights;
use Doctrine\DBAL\Connection;
use Generator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Jiminny\Component\DealInsights\Forecast\DealData;
use Jiminny\Component\DealInsights\Forecast\DealsFilter;
use Jiminny\Component\DealInsights\QueryBuilder\QueryBuilder;
use Jiminny\Component\DealInsights\QueryBuilder\Visitor\QueryBuilderVisitorInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Services\Crm\IntegrationApp\DTO\Utils\UrlGeneratorInterface;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class DealsRepository implements DealsRepositoryInterface
{
private Connection $connection;
private ProviderRegistry $providerRegistry;
/**
* @var QueryBuilderVisitorInterface[]
*/
private array $visitors = [];
/**
* @param QueryBuilderVisitorInterface[] $visitors
*/
public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])
{
$this->connection = $connection;
$this->providerRegistry = $crmProviderRegistry;
foreach ($visitors as $visitor) {
$this->visitors[$visitor->getIdentifier()] = $visitor;
}
}
public function getDeals(CriteriaInterface $criteria): array
{
$context = $criteria->getContext();
$team = $context->getTeam();
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$this->visit($qb, $criteria);
return $this->execute($team, $crmService, $qb);
}
public function getDeal(Team $team, int $id): array
{
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$qb->andWhere('opp.id = :id')->setParameter('id', $id);
return $this->execute($team, $crmService, $qb);
}
public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])
{
$qb = new QueryBuilder($this->connection);
$qb
->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')
->from('crm_fields', 'f')
->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')
->where('f.crm_configuration_id = :crm')
->andWhere('f.object_type = :type')
->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')
->orderBy('fd.object_id', 'ASC')
->addOrderBy('fd.updated_at', 'ASC')
->setParameter('type', Field::OBJECT_OPPORTUNITY)
->setParameter('crm', $crmId)
;
if (! empty($crmFields)) {
$fields = array_map(fn ($value): string => '"' . $value . '"', $crmFields);
$qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');
}
return $qb->executeQuery()->fetchAllAssociative();
}
public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAssociative();
}
public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('COALESCE(opp.currency_code, "' . $defaultCurrency . '") AS currency')
->addSelect('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
->groupBy('currency')
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAllAssociative();
}
public function getDealActivities(CriteriaInterface $criteria): array
{
$qb = Activity::with(['participants', 'user'])
->where('opportunity_id', $criteria->getOpportunityId())
->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())
->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())
->orderBy($criteria->getSortBy(), $criteria->getSortDirection())
;
// Should we filter activities by criteria? It's intended to filter deals.
return $qb->get()->all();
}
public function getStages(CriteriaInterface $criteria): array
{
$qb = new QueryBuilder($this->connection);
$qb
->select('id', 'label', 'sequence')
->from('stages', 's')
->where('crm_configuration_id = :crm_configuration_id')
->andWhere('type = :type')
->orderBy('sequence', 'ASC')
->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())
->setParameter('type', Stage::TYPE_OPPORTUNITY);
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$result[$row['id']] = [
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
public function getConfigurationStages(Configuration $configuration): Collection
{
return $configuration
->stages()
->where('type', Stage::TYPE_OPPORTUNITY)
->get();
}
public function getPipelineData(Configuration $crm): array
{
$qb = new QueryBuilder($this->connection);
$provider = $crm->provider;
$qb
->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')
->from('stages', 's')
->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')
->where('s.crm_configuration_id = :crm_configuration_id')
->andWhere('s.type = :type')
->orderBy('bps.business_process_id', 'ASC')
->addOrderBy('s.sequence', 'ASC')
->setParameter('crm_configuration_id', $crm->id)
->setParameter('type', Stage::TYPE_OPPORTUNITY)
;
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];
$result[$row['pipeline_id']][] = [
'value' => $value,
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
private function createQueryBuilder(string $realm): QueryBuilder
{
return (new QueryBuilder($this->connection))
->setRealm($realm)
->from('opportunities', 'opp')
->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')
->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')
->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')
;
}
/**
* Applies all applicable visitors and returns the IDs of the executed ones
*
* @return string[]
*/
private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array
{
$queryVisitors = [];
foreach ($this->visitors as $visitor) {
if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {
$visitor->visit($queryBuilder, $criteria);
$queryVisitors[] = $visitor->getIdentifier();
}
}
return $queryVisitors;
}
private function hydrateStages(array $deals): array
{
foreach ($this->fetchStages(array_keys($deals)) as $stage) {
$oppId = (int) $stage['opportunity_id'];
if (! isset($deals[$oppId])) {
continue; // or throw??!
}
$deals[$oppId]['stages'][] = [
'id' => $stage['stage_id'],
'name' => $stage['label'],
'enteredAt' => $stage['created_at'],
];
}
return $deals;
}
/**
* @param int[] $dealIds
*/
private function fetchStages(array $dealIds): array
{
if (empty($dealIds)) {
return [];
}
$qb = new QueryBuilder($this->connection);
$qb
->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')
->from('opportunity_stages', 'os')
->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')
->where($qb->expr()->in('os.opportunity_id', $dealIds))
->orderBy('os.opportunity_id', 'ASC')
->addOrderBy('s.created_at', 'ASC')
;
return $qb->executeQuery()->fetchAllAssociative();
}
private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array
{
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$data = [
'uuid' => RequiresUUID::toNormal($row['uuid']),
'name' => $row['name'],
'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),
'account' => [
'name' => $row['acc_name'],
'url' => $crmService->generateProviderUrl(
providerId: $row['acc_provider_id'],
objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'
),
],
'owner' => null,
'rawValue' => [
'amount' => (float) $row['value'],
'currency' => $row['currency_code'],
],
'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),
'openDate' => $row['remotely_created_at'] ?? null,
'closeDate' => $row['close_date'] ?? null,
'stages' => [],
'currentPipelineId' => $row['pipeline_id'],
'currentStage' => [
'id' => $row['stage_id'],
'enteredAt' => $row['stage_updated_at'],
],
'currentStageUpdatedAt' => $row['stage_updated_at'],
'isClosed' => (bool) $row['is_closed'],
'isWon' => (bool) $row['is_won'],
];
if (isset($row['owner_uuid'])) {
$data['owner'] = [
'uuid' => RequiresUUID::toNormal($row['owner_uuid']),
'name' => $row['owner_name'],
'photoUrl' => $row['owner_photo'] === null
? null
: client_cdn($row['owner_photo'], $team),
'id' => $row['owner_id'],
'job' => $row['owner_job'],
];
}
$result[(int) $row['opp_id']] = $data;
}
return $this->hydrateStages($result);
}
private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder
{
$qb = clone $queryBuilder;
$qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');
$qb
->select(...[
'opp.id as opp_id',
'opp.uuid',
'opp.name',
'opp.value',
'opp.currency_code',
'opp.close_date',
'opp.remotely_created_at',
'opp.is_closed',
'opp.is_won',
])
->addSelect(...[
'usr.uuid as owner_uuid',
'usr.name AS owner_name',
'usr.photo_path as owner_photo',
'usr.id AS owner_id',
'jt.name as owner_job',
])
->addSelect('opp.stage_id', 'opp.stage_updated_at')
->addSelect(...[
'acc.name AS acc_name',
'acc.is_internal as acc_is_internal',
'opp.stage_updated_at',
'acc.crm_provider_id AS acc_provider_id',
'opp.crm_provider_id AS opp_provider_id',
])
->addSelect('rt.business_process_id AS pipeline_id')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'));
return $qb;
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws SocialAccountTokenInvalidException
*/
private function getCrmService(Team $team): ServiceInterface
{
$crmService = $this->providerRegistry->get($team->crm->provider);
$crmService->setConfiguration($team->crm);
if ($crmService instanceof UrlGeneratorInterface) {
$crmService->setCrmUrlGenerator($team->crm);
}
return $crmService;
}
/**
*
* @return Generator<DealData>
*/
public function getForecastData(DealsFilter $filter): Generator
{
$opportunities = DB::query()
->select([
'o.value',
'o.close_date',
'o.currency_code',
'o.is_won',
'o.is_closed',
'o.probability',
'o.forecast_category',
])
->from('opportunities', 'o')
->join('users', 'users.id', '=', 'o.user_id')
->join('groups', 'groups.id', '=', 'users.group_id')
->where('users.team_id', $filter->getTeam()->getId())
->where('o.close_date', '>=', $filter->getStartDate())
->where('o.close_date', '<=', $filter->getEndDate())
->where('o.currency_code', $filter->getCurrency())
->where('o.deleted_at', '=', null)
;
$userUuidList = $filter->getUserUuidList();
if (! empty($userUuidList)) {
$userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);
$opportunities->whereIn('users.uuid', $userUuidList);
}
$groupUuidList = $filter->getGroupUuidList();
if (! empty($groupUuidList)) {
$groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);
$opportunities->whereIn('groups.uuid', $groupUuidList);
}
foreach ($opportunities->cursor() as $row) {
yield new DealData(
(float) $row->value,
$row->close_date,
! empty($row->is_won),
! empty($row->is_closed),
$row->probability ?: 0,
$row->forecast_category ?: '',
);
}
}
public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection
{
return $user->subscriptionSets()
->where(static function (Eloquent\Builder $query): void {
$query
->whereNull('expired_at')
->orWhere('expired_at', '>=', now());
})
->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {
$join
->on('subscription_set_id', '=', 'activity_subscription_sets.id');
$join
->where('followable_type', Models\Activity\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)
->whereIn('followable_id', $opportunityIds);
})
->pluck('followable_id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.25731382,"top":0.019952115,"width":0.03856383,"height":0.025538707},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20157-AJ-report-not-send-notification, menu","depth":5,"bounds":{"left":0.29587767,"top":0.019952115,"width":0.10139628,"height":0.025538707},"help_text":"Git Branch: JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.7972075,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"RequestGenerateAskJiminnyReportJobTest","depth":6,"bounds":{"left":0.8125,"top":0.019952115,"width":0.10305851,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"16","depth":4,"bounds":{"left":0.6030585,"top":0.17478053,"width":0.009640957,"height":0.015163607},"role_description":"text"},{"role":"AXStaticText","text":"7","depth":4,"bounds":{"left":0.6146942,"top":0.17478053,"width":0.0076462766,"height":0.015163607},"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.6243351,"top":0.17478053,"width":0.00731383,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.63331115,"top":0.17318435,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.640625,"top":0.17318435,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active Ask Jiminny reports whose expiry date has passed.\n *\n * @return Collection<AutomatedReport>\n */\n public function getExpiredActiveAskJiminnyReports(): Collection\n {\n return AutomatedReport::where('status', true)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereNotNull('expires_at')\n ->where('expires_at', '<', now()->toDateString())\n ->get();\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('report_id', $report->getId())\n ->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])\n ->latest()\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Restrict a query on the automated_reports table to reports the given user is allowed to see.\n *\n * Matches the customer-facing audience:\n * - explicit user recipients (recipients.users)\n * - members of any of the report's groups (Ask Jiminny reports)\n */\n private function applyUserAccessScope(Builder $query, User $user): void\n {\n $userId = $user->getId();\n $groupId = $user->getGroupId();\n\n $query\n ->where('automated_reports.team_id', $user->getTeamId())\n ->where(function (Builder $q) use ($userId, $groupId): void {\n $q->whereJsonContains('automated_reports.recipients->users', $userId);\n\n if ($groupId !== null) {\n $q->orWhere(function (Builder $sub) use ($groupId): void {\n $sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereJsonContains('automated_reports.groups', $groupId);\n });\n }\n });\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active Ask Jiminny reports whose expiry date has passed.\n *\n * @return Collection<AutomatedReport>\n */\n public function getExpiredActiveAskJiminnyReports(): Collection\n {\n return AutomatedReport::where('status', true)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereNotNull('expires_at')\n ->where('expires_at', '<', now()->toDateString())\n ->get();\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('report_id', $report->getId())\n ->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])\n ->latest()\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Restrict a query on the automated_reports table to reports the given user is allowed to see.\n *\n * Matches the customer-facing audience:\n * - explicit user recipients (recipients.users)\n * - members of any of the report's groups (Ask Jiminny reports)\n */\n private function applyUserAccessScope(Builder $query, User $user): void\n {\n $userId = $user->getId();\n $groupId = $user->getGroupId();\n\n $query\n ->where('automated_reports.team_id', $user->getTeamId())\n ->where(function (Builder $q) use ($userId, $groupId): void {\n $q->whereJsonContains('automated_reports.recipients->users', $userId);\n\n if ($groupId !== null) {\n $q->orWhere(function (Builder $sub) use ($groupId): void {\n $sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereJsonContains('automated_reports.groups', $groupId);\n });\n }\n });\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"36","depth":4,"bounds":{"left":0.69980055,"top":0.2490024,"width":0.010305851,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7117686,"top":0.24740623,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7190825,"top":0.24740623,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\DealInsights;\n\nuse Doctrine\\DBAL\\Connection;\nuse Generator;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealData;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealsFilter;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\QueryBuilder;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\Visitor\\QueryBuilderVisitorInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Services\\Crm\\IntegrationApp\\DTO\\Utils\\UrlGeneratorInterface;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Eloquent;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass DealsRepository implements DealsRepositoryInterface\n{\n private Connection $connection;\n\n private ProviderRegistry $providerRegistry;\n\n /**\n * @var QueryBuilderVisitorInterface[]\n */\n private array $visitors = [];\n\n /**\n * @param QueryBuilderVisitorInterface[] $visitors\n */\n public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])\n {\n $this->connection = $connection;\n $this->providerRegistry = $crmProviderRegistry;\n\n foreach ($visitors as $visitor) {\n $this->visitors[$visitor->getIdentifier()] = $visitor;\n }\n }\n\n public function getDeals(CriteriaInterface $criteria): array\n {\n $context = $criteria->getContext();\n $team = $context->getTeam();\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n\n $this->visit($qb, $criteria);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getDeal(Team $team, int $id): array\n {\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n $qb->andWhere('opp.id = :id')->setParameter('id', $id);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')\n ->from('crm_fields', 'f')\n ->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')\n ->where('f.crm_configuration_id = :crm')\n ->andWhere('f.object_type = :type')\n ->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')\n ->orderBy('fd.object_id', 'ASC')\n ->addOrderBy('fd.updated_at', 'ASC')\n\n ->setParameter('type', Field::OBJECT_OPPORTUNITY)\n ->setParameter('crm', $crmId)\n ;\n\n if (! empty($crmFields)) {\n $fields = array_map(fn ($value): string => '\"' . $value . '\"', $crmFields);\n $qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');\n }\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAssociative();\n }\n\n public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('COALESCE(opp.currency_code, \"' . $defaultCurrency . '\") AS currency')\n ->addSelect('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ->groupBy('currency')\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getDealActivities(CriteriaInterface $criteria): array\n {\n $qb = Activity::with(['participants', 'user'])\n ->where('opportunity_id', $criteria->getOpportunityId())\n ->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())\n ->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())\n ->orderBy($criteria->getSortBy(), $criteria->getSortDirection())\n ;\n\n // Should we filter activities by criteria? It's intended to filter deals.\n\n return $qb->get()->all();\n }\n\n public function getStages(CriteriaInterface $criteria): array\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('id', 'label', 'sequence')\n ->from('stages', 's')\n ->where('crm_configuration_id = :crm_configuration_id')\n ->andWhere('type = :type')\n ->orderBy('sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())\n ->setParameter('type', Stage::TYPE_OPPORTUNITY);\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $result[$row['id']] = [\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n public function getConfigurationStages(Configuration $configuration): Collection\n {\n return $configuration\n ->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get();\n }\n\n public function getPipelineData(Configuration $crm): array\n {\n $qb = new QueryBuilder($this->connection);\n $provider = $crm->provider;\n\n $qb\n ->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')\n ->from('stages', 's')\n ->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')\n ->where('s.crm_configuration_id = :crm_configuration_id')\n ->andWhere('s.type = :type')\n ->orderBy('bps.business_process_id', 'ASC')\n ->addOrderBy('s.sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $crm->id)\n ->setParameter('type', Stage::TYPE_OPPORTUNITY)\n ;\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];\n $result[$row['pipeline_id']][] = [\n 'value' => $value,\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n private function createQueryBuilder(string $realm): QueryBuilder\n {\n return (new QueryBuilder($this->connection))\n ->setRealm($realm)\n ->from('opportunities', 'opp')\n ->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')\n ->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')\n ->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')\n ;\n }\n\n /**\n * Applies all applicable visitors and returns the IDs of the executed ones\n *\n * @return string[]\n */\n private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array\n {\n $queryVisitors = [];\n\n foreach ($this->visitors as $visitor) {\n if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {\n $visitor->visit($queryBuilder, $criteria);\n\n $queryVisitors[] = $visitor->getIdentifier();\n }\n }\n\n return $queryVisitors;\n }\n\n private function hydrateStages(array $deals): array\n {\n foreach ($this->fetchStages(array_keys($deals)) as $stage) {\n $oppId = (int) $stage['opportunity_id'];\n\n if (! isset($deals[$oppId])) {\n continue; // or throw??!\n }\n\n $deals[$oppId]['stages'][] = [\n 'id' => $stage['stage_id'],\n 'name' => $stage['label'],\n 'enteredAt' => $stage['created_at'],\n ];\n }\n\n return $deals;\n }\n\n /**\n * @param int[] $dealIds\n */\n private function fetchStages(array $dealIds): array\n {\n if (empty($dealIds)) {\n return [];\n }\n\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')\n ->from('opportunity_stages', 'os')\n ->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')\n ->where($qb->expr()->in('os.opportunity_id', $dealIds))\n ->orderBy('os.opportunity_id', 'ASC')\n ->addOrderBy('s.created_at', 'ASC')\n ;\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array\n {\n $result = [];\n\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $data = [\n 'uuid' => RequiresUUID::toNormal($row['uuid']),\n 'name' => $row['name'],\n 'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),\n 'account' => [\n 'name' => $row['acc_name'],\n 'url' => $crmService->generateProviderUrl(\n providerId: $row['acc_provider_id'],\n objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'\n ),\n ],\n 'owner' => null,\n 'rawValue' => [\n 'amount' => (float) $row['value'],\n 'currency' => $row['currency_code'],\n ],\n 'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),\n 'openDate' => $row['remotely_created_at'] ?? null,\n 'closeDate' => $row['close_date'] ?? null,\n 'stages' => [],\n 'currentPipelineId' => $row['pipeline_id'],\n 'currentStage' => [\n 'id' => $row['stage_id'],\n 'enteredAt' => $row['stage_updated_at'],\n ],\n 'currentStageUpdatedAt' => $row['stage_updated_at'],\n 'isClosed' => (bool) $row['is_closed'],\n 'isWon' => (bool) $row['is_won'],\n ];\n\n if (isset($row['owner_uuid'])) {\n $data['owner'] = [\n 'uuid' => RequiresUUID::toNormal($row['owner_uuid']),\n 'name' => $row['owner_name'],\n 'photoUrl' => $row['owner_photo'] === null\n ? null\n : client_cdn($row['owner_photo'], $team),\n 'id' => $row['owner_id'],\n 'job' => $row['owner_job'],\n ];\n }\n\n $result[(int) $row['opp_id']] = $data;\n }\n\n return $this->hydrateStages($result);\n }\n\n private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder\n {\n $qb = clone $queryBuilder;\n $qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');\n\n $qb\n ->select(...[\n 'opp.id as opp_id',\n 'opp.uuid',\n 'opp.name',\n 'opp.value',\n 'opp.currency_code',\n 'opp.close_date',\n 'opp.remotely_created_at',\n 'opp.is_closed',\n 'opp.is_won',\n ])\n ->addSelect(...[\n 'usr.uuid as owner_uuid',\n 'usr.name AS owner_name',\n 'usr.photo_path as owner_photo',\n 'usr.id AS owner_id',\n 'jt.name as owner_job',\n ])\n ->addSelect('opp.stage_id', 'opp.stage_updated_at')\n ->addSelect(...[\n 'acc.name AS acc_name',\n 'acc.is_internal as acc_is_internal',\n 'opp.stage_updated_at',\n 'acc.crm_provider_id AS acc_provider_id',\n 'opp.crm_provider_id AS opp_provider_id',\n ])\n ->addSelect('rt.business_process_id AS pipeline_id')\n\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'));\n\n return $qb;\n }\n\n /**\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws SocialAccountTokenInvalidException\n */\n private function getCrmService(Team $team): ServiceInterface\n {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n $crmService->setConfiguration($team->crm);\n if ($crmService instanceof UrlGeneratorInterface) {\n $crmService->setCrmUrlGenerator($team->crm);\n }\n\n return $crmService;\n }\n\n /**\n *\n * @return Generator<DealData>\n */\n public function getForecastData(DealsFilter $filter): Generator\n {\n $opportunities = DB::query()\n ->select([\n 'o.value',\n 'o.close_date',\n 'o.currency_code',\n 'o.is_won',\n 'o.is_closed',\n 'o.probability',\n 'o.forecast_category',\n ])\n ->from('opportunities', 'o')\n ->join('users', 'users.id', '=', 'o.user_id')\n ->join('groups', 'groups.id', '=', 'users.group_id')\n ->where('users.team_id', $filter->getTeam()->getId())\n ->where('o.close_date', '>=', $filter->getStartDate())\n ->where('o.close_date', '<=', $filter->getEndDate())\n ->where('o.currency_code', $filter->getCurrency())\n ->where('o.deleted_at', '=', null)\n ;\n\n $userUuidList = $filter->getUserUuidList();\n if (! empty($userUuidList)) {\n $userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);\n\n $opportunities->whereIn('users.uuid', $userUuidList);\n }\n\n $groupUuidList = $filter->getGroupUuidList();\n if (! empty($groupUuidList)) {\n $groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);\n\n $opportunities->whereIn('groups.uuid', $groupUuidList);\n }\n\n foreach ($opportunities->cursor() as $row) {\n yield new DealData(\n (float) $row->value,\n $row->close_date,\n ! empty($row->is_won),\n ! empty($row->is_closed),\n $row->probability ?: 0,\n $row->forecast_category ?: '',\n );\n }\n }\n\n public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection\n {\n return $user->subscriptionSets()\n ->where(static function (Eloquent\\Builder $query): void {\n $query\n ->whereNull('expired_at')\n ->orWhere('expired_at', '>=', now());\n })\n ->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n $join\n ->where('followable_type', Models\\Activity\\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)\n ->whereIn('followable_id', $opportunityIds);\n })\n ->pluck('followable_id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\DealInsights;\n\nuse Doctrine\\DBAL\\Connection;\nuse Generator;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealData;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealsFilter;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\QueryBuilder;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\Visitor\\QueryBuilderVisitorInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Services\\Crm\\IntegrationApp\\DTO\\Utils\\UrlGeneratorInterface;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Eloquent;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass DealsRepository implements DealsRepositoryInterface\n{\n private Connection $connection;\n\n private ProviderRegistry $providerRegistry;\n\n /**\n * @var QueryBuilderVisitorInterface[]\n */\n private array $visitors = [];\n\n /**\n * @param QueryBuilderVisitorInterface[] $visitors\n */\n public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])\n {\n $this->connection = $connection;\n $this->providerRegistry = $crmProviderRegistry;\n\n foreach ($visitors as $visitor) {\n $this->visitors[$visitor->getIdentifier()] = $visitor;\n }\n }\n\n public function getDeals(CriteriaInterface $criteria): array\n {\n $context = $criteria->getContext();\n $team = $context->getTeam();\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n\n $this->visit($qb, $criteria);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getDeal(Team $team, int $id): array\n {\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n $qb->andWhere('opp.id = :id')->setParameter('id', $id);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')\n ->from('crm_fields', 'f')\n ->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')\n ->where('f.crm_configuration_id = :crm')\n ->andWhere('f.object_type = :type')\n ->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')\n ->orderBy('fd.object_id', 'ASC')\n ->addOrderBy('fd.updated_at', 'ASC')\n\n ->setParameter('type', Field::OBJECT_OPPORTUNITY)\n ->setParameter('crm', $crmId)\n ;\n\n if (! empty($crmFields)) {\n $fields = array_map(fn ($value): string => '\"' . $value . '\"', $crmFields);\n $qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');\n }\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAssociative();\n }\n\n public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('COALESCE(opp.currency_code, \"' . $defaultCurrency . '\") AS currency')\n ->addSelect('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ->groupBy('currency')\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getDealActivities(CriteriaInterface $criteria): array\n {\n $qb = Activity::with(['participants', 'user'])\n ->where('opportunity_id', $criteria->getOpportunityId())\n ->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())\n ->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())\n ->orderBy($criteria->getSortBy(), $criteria->getSortDirection())\n ;\n\n // Should we filter activities by criteria? It's intended to filter deals.\n\n return $qb->get()->all();\n }\n\n public function getStages(CriteriaInterface $criteria): array\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('id', 'label', 'sequence')\n ->from('stages', 's')\n ->where('crm_configuration_id = :crm_configuration_id')\n ->andWhere('type = :type')\n ->orderBy('sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())\n ->setParameter('type', Stage::TYPE_OPPORTUNITY);\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $result[$row['id']] = [\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n public function getConfigurationStages(Configuration $configuration): Collection\n {\n return $configuration\n ->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get();\n }\n\n public function getPipelineData(Configuration $crm): array\n {\n $qb = new QueryBuilder($this->connection);\n $provider = $crm->provider;\n\n $qb\n ->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')\n ->from('stages', 's')\n ->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')\n ->where('s.crm_configuration_id = :crm_configuration_id')\n ->andWhere('s.type = :type')\n ->orderBy('bps.business_process_id', 'ASC')\n ->addOrderBy('s.sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $crm->id)\n ->setParameter('type', Stage::TYPE_OPPORTUNITY)\n ;\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];\n $result[$row['pipeline_id']][] = [\n 'value' => $value,\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n private function createQueryBuilder(string $realm): QueryBuilder\n {\n return (new QueryBuilder($this->connection))\n ->setRealm($realm)\n ->from('opportunities', 'opp')\n ->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')\n ->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')\n ->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')\n ;\n }\n\n /**\n * Applies all applicable visitors and returns the IDs of the executed ones\n *\n * @return string[]\n */\n private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array\n {\n $queryVisitors = [];\n\n foreach ($this->visitors as $visitor) {\n if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {\n $visitor->visit($queryBuilder, $criteria);\n\n $queryVisitors[] = $visitor->getIdentifier();\n }\n }\n\n return $queryVisitors;\n }\n\n private function hydrateStages(array $deals): array\n {\n foreach ($this->fetchStages(array_keys($deals)) as $stage) {\n $oppId = (int) $stage['opportunity_id'];\n\n if (! isset($deals[$oppId])) {\n continue; // or throw??!\n }\n\n $deals[$oppId]['stages'][] = [\n 'id' => $stage['stage_id'],\n 'name' => $stage['label'],\n 'enteredAt' => $stage['created_at'],\n ];\n }\n\n return $deals;\n }\n\n /**\n * @param int[] $dealIds\n */\n private function fetchStages(array $dealIds): array\n {\n if (empty($dealIds)) {\n return [];\n }\n\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')\n ->from('opportunity_stages', 'os')\n ->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')\n ->where($qb->expr()->in('os.opportunity_id', $dealIds))\n ->orderBy('os.opportunity_id', 'ASC')\n ->addOrderBy('s.created_at', 'ASC')\n ;\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array\n {\n $result = [];\n\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $data = [\n 'uuid' => RequiresUUID::toNormal($row['uuid']),\n 'name' => $row['name'],\n 'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),\n 'account' => [\n 'name' => $row['acc_name'],\n 'url' => $crmService->generateProviderUrl(\n providerId: $row['acc_provider_id'],\n objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'\n ),\n ],\n 'owner' => null,\n 'rawValue' => [\n 'amount' => (float) $row['value'],\n 'currency' => $row['currency_code'],\n ],\n 'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),\n 'openDate' => $row['remotely_created_at'] ?? null,\n 'closeDate' => $row['close_date'] ?? null,\n 'stages' => [],\n 'currentPipelineId' => $row['pipeline_id'],\n 'currentStage' => [\n 'id' => $row['stage_id'],\n 'enteredAt' => $row['stage_updated_at'],\n ],\n 'currentStageUpdatedAt' => $row['stage_updated_at'],\n 'isClosed' => (bool) $row['is_closed'],\n 'isWon' => (bool) $row['is_won'],\n ];\n\n if (isset($row['owner_uuid'])) {\n $data['owner'] = [\n 'uuid' => RequiresUUID::toNormal($row['owner_uuid']),\n 'name' => $row['owner_name'],\n 'photoUrl' => $row['owner_photo'] === null\n ? null\n : client_cdn($row['owner_photo'], $team),\n 'id' => $row['owner_id'],\n 'job' => $row['owner_job'],\n ];\n }\n\n $result[(int) $row['opp_id']] = $data;\n }\n\n return $this->hydrateStages($result);\n }\n\n private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder\n {\n $qb = clone $queryBuilder;\n $qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');\n\n $qb\n ->select(...[\n 'opp.id as opp_id',\n 'opp.uuid',\n 'opp.name',\n 'opp.value',\n 'opp.currency_code',\n 'opp.close_date',\n 'opp.remotely_created_at',\n 'opp.is_closed',\n 'opp.is_won',\n ])\n ->addSelect(...[\n 'usr.uuid as owner_uuid',\n 'usr.name AS owner_name',\n 'usr.photo_path as owner_photo',\n 'usr.id AS owner_id',\n 'jt.name as owner_job',\n ])\n ->addSelect('opp.stage_id', 'opp.stage_updated_at')\n ->addSelect(...[\n 'acc.name AS acc_name',\n 'acc.is_internal as acc_is_internal',\n 'opp.stage_updated_at',\n 'acc.crm_provider_id AS acc_provider_id',\n 'opp.crm_provider_id AS opp_provider_id',\n ])\n ->addSelect('rt.business_process_id AS pipeline_id')\n\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'));\n\n return $qb;\n }\n\n /**\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws SocialAccountTokenInvalidException\n */\n private function getCrmService(Team $team): ServiceInterface\n {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n $crmService->setConfiguration($team->crm);\n if ($crmService instanceof UrlGeneratorInterface) {\n $crmService->setCrmUrlGenerator($team->crm);\n }\n\n return $crmService;\n }\n\n /**\n *\n * @return Generator<DealData>\n */\n public function getForecastData(DealsFilter $filter): Generator\n {\n $opportunities = DB::query()\n ->select([\n 'o.value',\n 'o.close_date',\n 'o.currency_code',\n 'o.is_won',\n 'o.is_closed',\n 'o.probability',\n 'o.forecast_category',\n ])\n ->from('opportunities', 'o')\n ->join('users', 'users.id', '=', 'o.user_id')\n ->join('groups', 'groups.id', '=', 'users.group_id')\n ->where('users.team_id', $filter->getTeam()->getId())\n ->where('o.close_date', '>=', $filter->getStartDate())\n ->where('o.close_date', '<=', $filter->getEndDate())\n ->where('o.currency_code', $filter->getCurrency())\n ->where('o.deleted_at', '=', null)\n ;\n\n $userUuidList = $filter->getUserUuidList();\n if (! empty($userUuidList)) {\n $userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);\n\n $opportunities->whereIn('users.uuid', $userUuidList);\n }\n\n $groupUuidList = $filter->getGroupUuidList();\n if (! empty($groupUuidList)) {\n $groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);\n\n $opportunities->whereIn('groups.uuid', $groupUuidList);\n }\n\n foreach ($opportunities->cursor() as $row) {\n yield new DealData(\n (float) $row->value,\n $row->close_date,\n ! empty($row->is_won),\n ! empty($row->is_closed),\n $row->probability ?: 0,\n $row->forecast_category ?: '',\n );\n }\n }\n\n public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection\n {\n return $user->subscriptionSets()\n ->where(static function (Eloquent\\Builder $query): void {\n $query\n ->whereNull('expired_at')\n ->orWhere('expired_at', '>=', now());\n })\n ->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n $join\n ->where('followable_type', Models\\Activity\\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)\n ->whereIn('followable_id', $opportunityIds);\n })\n ->pluck('followable_id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.24335106,"top":0.047885075,"width":0.024268618,"height":0.024740623},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3407362332434637772
|
-4167466460282672524
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
16
7
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active Ask Jiminny reports whose expiry date has passed.
*
* @return Collection<AutomatedReport>
*/
public function getExpiredActiveAskJiminnyReports(): Collection
{
return AutomatedReport::where('status', true)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereNotNull('expires_at')
->where('expires_at', '<', now()->toDateString())
->get();
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('report_id', $report->getId())
->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])
->latest()
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Restrict a query on the automated_reports table to reports the given user is allowed to see.
*
* Matches the customer-facing audience:
* - explicit user recipients (recipients.users)
* - members of any of the report's groups (Ask Jiminny reports)
*/
private function applyUserAccessScope(Builder $query, User $user): void
{
$userId = $user->getId();
$groupId = $user->getGroupId();
$query
->where('automated_reports.team_id', $user->getTeamId())
->where(function (Builder $q) use ($userId, $groupId): void {
$q->whereJsonContains('automated_reports.recipients->users', $userId);
if ($groupId !== null) {
$q->orWhere(function (Builder $sub) use ($groupId): void {
$sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereJsonContains('automated_reports.groups', $groupId);
});
}
});
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
36
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\DealInsights;
use Doctrine\DBAL\Connection;
use Generator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Jiminny\Component\DealInsights\Forecast\DealData;
use Jiminny\Component\DealInsights\Forecast\DealsFilter;
use Jiminny\Component\DealInsights\QueryBuilder\QueryBuilder;
use Jiminny\Component\DealInsights\QueryBuilder\Visitor\QueryBuilderVisitorInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Services\Crm\IntegrationApp\DTO\Utils\UrlGeneratorInterface;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class DealsRepository implements DealsRepositoryInterface
{
private Connection $connection;
private ProviderRegistry $providerRegistry;
/**
* @var QueryBuilderVisitorInterface[]
*/
private array $visitors = [];
/**
* @param QueryBuilderVisitorInterface[] $visitors
*/
public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])
{
$this->connection = $connection;
$this->providerRegistry = $crmProviderRegistry;
foreach ($visitors as $visitor) {
$this->visitors[$visitor->getIdentifier()] = $visitor;
}
}
public function getDeals(CriteriaInterface $criteria): array
{
$context = $criteria->getContext();
$team = $context->getTeam();
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$this->visit($qb, $criteria);
return $this->execute($team, $crmService, $qb);
}
public function getDeal(Team $team, int $id): array
{
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$qb->andWhere('opp.id = :id')->setParameter('id', $id);
return $this->execute($team, $crmService, $qb);
}
public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])
{
$qb = new QueryBuilder($this->connection);
$qb
->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')
->from('crm_fields', 'f')
->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')
->where('f.crm_configuration_id = :crm')
->andWhere('f.object_type = :type')
->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')
->orderBy('fd.object_id', 'ASC')
->addOrderBy('fd.updated_at', 'ASC')
->setParameter('type', Field::OBJECT_OPPORTUNITY)
->setParameter('crm', $crmId)
;
if (! empty($crmFields)) {
$fields = array_map(fn ($value): string => '"' . $value . '"', $crmFields);
$qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');
}
return $qb->executeQuery()->fetchAllAssociative();
}
public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAssociative();
}
public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('COALESCE(opp.currency_code, "' . $defaultCurrency . '") AS currency')
->addSelect('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
->groupBy('currency')
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAllAssociative();
}
public function getDealActivities(CriteriaInterface $criteria): array
{
$qb = Activity::with(['participants', 'user'])
->where('opportunity_id', $criteria->getOpportunityId())
->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())
->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())
->orderBy($criteria->getSortBy(), $criteria->getSortDirection())
;
// Should we filter activities by criteria? It's intended to filter deals.
return $qb->get()->all();
}
public function getStages(CriteriaInterface $criteria): array
{
$qb = new QueryBuilder($this->connection);
$qb
->select('id', 'label', 'sequence')
->from('stages', 's')
->where('crm_configuration_id = :crm_configuration_id')
->andWhere('type = :type')
->orderBy('sequence', 'ASC')
->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())
->setParameter('type', Stage::TYPE_OPPORTUNITY);
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$result[$row['id']] = [
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
public function getConfigurationStages(Configuration $configuration): Collection
{
return $configuration
->stages()
->where('type', Stage::TYPE_OPPORTUNITY)
->get();
}
public function getPipelineData(Configuration $crm): array
{
$qb = new QueryBuilder($this->connection);
$provider = $crm->provider;
$qb
->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')
->from('stages', 's')
->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')
->where('s.crm_configuration_id = :crm_configuration_id')
->andWhere('s.type = :type')
->orderBy('bps.business_process_id', 'ASC')
->addOrderBy('s.sequence', 'ASC')
->setParameter('crm_configuration_id', $crm->id)
->setParameter('type', Stage::TYPE_OPPORTUNITY)
;
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];
$result[$row['pipeline_id']][] = [
'value' => $value,
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
private function createQueryBuilder(string $realm): QueryBuilder
{
return (new QueryBuilder($this->connection))
->setRealm($realm)
->from('opportunities', 'opp')
->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')
->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')
->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')
;
}
/**
* Applies all applicable visitors and returns the IDs of the executed ones
*
* @return string[]
*/
private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array
{
$queryVisitors = [];
foreach ($this->visitors as $visitor) {
if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {
$visitor->visit($queryBuilder, $criteria);
$queryVisitors[] = $visitor->getIdentifier();
}
}
return $queryVisitors;
}
private function hydrateStages(array $deals): array
{
foreach ($this->fetchStages(array_keys($deals)) as $stage) {
$oppId = (int) $stage['opportunity_id'];
if (! isset($deals[$oppId])) {
continue; // or throw??!
}
$deals[$oppId]['stages'][] = [
'id' => $stage['stage_id'],
'name' => $stage['label'],
'enteredAt' => $stage['created_at'],
];
}
return $deals;
}
/**
* @param int[] $dealIds
*/
private function fetchStages(array $dealIds): array
{
if (empty($dealIds)) {
return [];
}
$qb = new QueryBuilder($this->connection);
$qb
->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')
->from('opportunity_stages', 'os')
->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')
->where($qb->expr()->in('os.opportunity_id', $dealIds))
->orderBy('os.opportunity_id', 'ASC')
->addOrderBy('s.created_at', 'ASC')
;
return $qb->executeQuery()->fetchAllAssociative();
}
private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array
{
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$data = [
'uuid' => RequiresUUID::toNormal($row['uuid']),
'name' => $row['name'],
'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),
'account' => [
'name' => $row['acc_name'],
'url' => $crmService->generateProviderUrl(
providerId: $row['acc_provider_id'],
objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'
),
],
'owner' => null,
'rawValue' => [
'amount' => (float) $row['value'],
'currency' => $row['currency_code'],
],
'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),
'openDate' => $row['remotely_created_at'] ?? null,
'closeDate' => $row['close_date'] ?? null,
'stages' => [],
'currentPipelineId' => $row['pipeline_id'],
'currentStage' => [
'id' => $row['stage_id'],
'enteredAt' => $row['stage_updated_at'],
],
'currentStageUpdatedAt' => $row['stage_updated_at'],
'isClosed' => (bool) $row['is_closed'],
'isWon' => (bool) $row['is_won'],
];
if (isset($row['owner_uuid'])) {
$data['owner'] = [
'uuid' => RequiresUUID::toNormal($row['owner_uuid']),
'name' => $row['owner_name'],
'photoUrl' => $row['owner_photo'] === null
? null
: client_cdn($row['owner_photo'], $team),
'id' => $row['owner_id'],
'job' => $row['owner_job'],
];
}
$result[(int) $row['opp_id']] = $data;
}
return $this->hydrateStages($result);
}
private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder
{
$qb = clone $queryBuilder;
$qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');
$qb
->select(...[
'opp.id as opp_id',
'opp.uuid',
'opp.name',
'opp.value',
'opp.currency_code',
'opp.close_date',
'opp.remotely_created_at',
'opp.is_closed',
'opp.is_won',
])
->addSelect(...[
'usr.uuid as owner_uuid',
'usr.name AS owner_name',
'usr.photo_path as owner_photo',
'usr.id AS owner_id',
'jt.name as owner_job',
])
->addSelect('opp.stage_id', 'opp.stage_updated_at')
->addSelect(...[
'acc.name AS acc_name',
'acc.is_internal as acc_is_internal',
'opp.stage_updated_at',
'acc.crm_provider_id AS acc_provider_id',
'opp.crm_provider_id AS opp_provider_id',
])
->addSelect('rt.business_process_id AS pipeline_id')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'));
return $qb;
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws SocialAccountTokenInvalidException
*/
private function getCrmService(Team $team): ServiceInterface
{
$crmService = $this->providerRegistry->get($team->crm->provider);
$crmService->setConfiguration($team->crm);
if ($crmService instanceof UrlGeneratorInterface) {
$crmService->setCrmUrlGenerator($team->crm);
}
return $crmService;
}
/**
*
* @return Generator<DealData>
*/
public function getForecastData(DealsFilter $filter): Generator
{
$opportunities = DB::query()
->select([
'o.value',
'o.close_date',
'o.currency_code',
'o.is_won',
'o.is_closed',
'o.probability',
'o.forecast_category',
])
->from('opportunities', 'o')
->join('users', 'users.id', '=', 'o.user_id')
->join('groups', 'groups.id', '=', 'users.group_id')
->where('users.team_id', $filter->getTeam()->getId())
->where('o.close_date', '>=', $filter->getStartDate())
->where('o.close_date', '<=', $filter->getEndDate())
->where('o.currency_code', $filter->getCurrency())
->where('o.deleted_at', '=', null)
;
$userUuidList = $filter->getUserUuidList();
if (! empty($userUuidList)) {
$userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);
$opportunities->whereIn('users.uuid', $userUuidList);
}
$groupUuidList = $filter->getGroupUuidList();
if (! empty($groupUuidList)) {
$groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);
$opportunities->whereIn('groups.uuid', $groupUuidList);
}
foreach ($opportunities->cursor() as $row) {
yield new DealData(
(float) $row->value,
$row->close_date,
! empty($row->is_won),
! empty($row->is_closed),
$row->probability ?: 0,
$row->forecast_category ?: '',
);
}
}
public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection
{
return $user->subscriptionSets()
->where(static function (Eloquent\Builder $query): void {
$query
->whereNull('expired_at')
->orWhere('expired_at', '>=', now());
})
->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {
$join
->on('subscription_set_id', '=', 'activity_subscription_sets.id');
$join
->where('followable_type', Models\Activity\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)
->whereIn('followable_id', $opportunityIds);
})
->pluck('followable_id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
72997
|
|
72986
|
NULL
|
0
|
2026-04-23T06:29:21.355212+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776925761355_m2.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsRepository.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
16
7
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active Ask Jiminny reports whose expiry date has passed.
*
* @return Collection<AutomatedReport>
*/
public function getExpiredActiveAskJiminnyReports(): Collection
{
return AutomatedReport::where('status', true)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereNotNull('expires_at')
->where('expires_at', '<', now()->toDateString())
->get();
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('report_id', $report->getId())
->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])
->latest()
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Restrict a query on the automated_reports table to reports the given user is allowed to see.
*
* Matches the customer-facing audience:
* - explicit user recipients (recipients.users)
* - members of any of the report's groups (Ask Jiminny reports)
*/
private function applyUserAccessScope(Builder $query, User $user): void
{
$userId = $user->getId();
$groupId = $user->getGroupId();
$query
->where('automated_reports.team_id', $user->getTeamId())
->where(function (Builder $q) use ($userId, $groupId): void {
$q->whereJsonContains('automated_reports.recipients->users', $userId);
if ($groupId !== null) {
$q->orWhere(function (Builder $sub) use ($groupId): void {
$sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereJsonContains('automated_reports.groups', $groupId);
});
}
});
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
36
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\DealInsights;
use Doctrine\DBAL\Connection;
use Generator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Jiminny\Component\DealInsights\Forecast\DealData;
use Jiminny\Component\DealInsights\Forecast\DealsFilter;
use Jiminny\Component\DealInsights\QueryBuilder\QueryBuilder;
use Jiminny\Component\DealInsights\QueryBuilder\Visitor\QueryBuilderVisitorInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Services\Crm\IntegrationApp\DTO\Utils\UrlGeneratorInterface;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class DealsRepository implements DealsRepositoryInterface
{
private Connection $connection;
private ProviderRegistry $providerRegistry;
/**
* @var QueryBuilderVisitorInterface[]
*/
private array $visitors = [];
/**
* @param QueryBuilderVisitorInterface[] $visitors
*/
public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])
{
$this->connection = $connection;
$this->providerRegistry = $crmProviderRegistry;
foreach ($visitors as $visitor) {
$this->visitors[$visitor->getIdentifier()] = $visitor;
}
}
public function getDeals(CriteriaInterface $criteria): array
{
$context = $criteria->getContext();
$team = $context->getTeam();
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$this->visit($qb, $criteria);
return $this->execute($team, $crmService, $qb);
}
public function getDeal(Team $team, int $id): array
{
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$qb->andWhere('opp.id = :id')->setParameter('id', $id);
return $this->execute($team, $crmService, $qb);
}
public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])
{
$qb = new QueryBuilder($this->connection);
$qb
->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')
->from('crm_fields', 'f')
->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')
->where('f.crm_configuration_id = :crm')
->andWhere('f.object_type = :type')
->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')
->orderBy('fd.object_id', 'ASC')
->addOrderBy('fd.updated_at', 'ASC')
->setParameter('type', Field::OBJECT_OPPORTUNITY)
->setParameter('crm', $crmId)
;
if (! empty($crmFields)) {
$fields = array_map(fn ($value): string => '"' . $value . '"', $crmFields);
$qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');
}
return $qb->executeQuery()->fetchAllAssociative();
}
public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAssociative();
}
public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('COALESCE(opp.currency_code, "' . $defaultCurrency . '") AS currency')
->addSelect('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
->groupBy('currency')
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAllAssociative();
}
public function getDealActivities(CriteriaInterface $criteria): array
{
$qb = Activity::with(['participants', 'user'])
->where('opportunity_id', $criteria->getOpportunityId())
->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())
->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())
->orderBy($criteria->getSortBy(), $criteria->getSortDirection())
;
// Should we filter activities by criteria? It's intended to filter deals.
return $qb->get()->all();
}
public function getStages(CriteriaInterface $criteria): array
{
$qb = new QueryBuilder($this->connection);
$qb
->select('id', 'label', 'sequence')
->from('stages', 's')
->where('crm_configuration_id = :crm_configuration_id')
->andWhere('type = :type')
->orderBy('sequence', 'ASC')
->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())
->setParameter('type', Stage::TYPE_OPPORTUNITY);
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$result[$row['id']] = [
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
public function getConfigurationStages(Configuration $configuration): Collection
{
return $configuration
->stages()
->where('type', Stage::TYPE_OPPORTUNITY)
->get();
}
public function getPipelineData(Configuration $crm): array
{
$qb = new QueryBuilder($this->connection);
$provider = $crm->provider;
$qb
->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')
->from('stages', 's')
->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')
->where('s.crm_configuration_id = :crm_configuration_id')
->andWhere('s.type = :type')
->orderBy('bps.business_process_id', 'ASC')
->addOrderBy('s.sequence', 'ASC')
->setParameter('crm_configuration_id', $crm->id)
->setParameter('type', Stage::TYPE_OPPORTUNITY)
;
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];
$result[$row['pipeline_id']][] = [
'value' => $value,
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
private function createQueryBuilder(string $realm): QueryBuilder
{
return (new QueryBuilder($this->connection))
->setRealm($realm)
->from('opportunities', 'opp')
->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')
->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')
->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')
;
}
/**
* Applies all applicable visitors and returns the IDs of the executed ones
*
* @return string[]
*/
private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array
{
$queryVisitors = [];
foreach ($this->visitors as $visitor) {
if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {
$visitor->visit($queryBuilder, $criteria);
$queryVisitors[] = $visitor->getIdentifier();
}
}
return $queryVisitors;
}
private function hydrateStages(array $deals): array
{
foreach ($this->fetchStages(array_keys($deals)) as $stage) {
$oppId = (int) $stage['opportunity_id'];
if (! isset($deals[$oppId])) {
continue; // or throw??!
}
$deals[$oppId]['stages'][] = [
'id' => $stage['stage_id'],
'name' => $stage['label'],
'enteredAt' => $stage['created_at'],
];
}
return $deals;
}
/**
* @param int[] $dealIds
*/
private function fetchStages(array $dealIds): array
{
if (empty($dealIds)) {
return [];
}
$qb = new QueryBuilder($this->connection);
$qb
->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')
->from('opportunity_stages', 'os')
->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')
->where($qb->expr()->in('os.opportunity_id', $dealIds))
->orderBy('os.opportunity_id', 'ASC')
->addOrderBy('s.created_at', 'ASC')
;
return $qb->executeQuery()->fetchAllAssociative();
}
private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array
{
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$data = [
'uuid' => RequiresUUID::toNormal($row['uuid']),
'name' => $row['name'],
'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),
'account' => [
'name' => $row['acc_name'],
'url' => $crmService->generateProviderUrl(
providerId: $row['acc_provider_id'],
objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'
),
],
'owner' => null,
'rawValue' => [
'amount' => (float) $row['value'],
'currency' => $row['currency_code'],
],
'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),
'openDate' => $row['remotely_created_at'] ?? null,
'closeDate' => $row['close_date'] ?? null,
'stages' => [],
'currentPipelineId' => $row['pipeline_id'],
'currentStage' => [
'id' => $row['stage_id'],
'enteredAt' => $row['stage_updated_at'],
],
'currentStageUpdatedAt' => $row['stage_updated_at'],
'isClosed' => (bool) $row['is_closed'],
'isWon' => (bool) $row['is_won'],
];
if (isset($row['owner_uuid'])) {
$data['owner'] = [
'uuid' => RequiresUUID::toNormal($row['owner_uuid']),
'name' => $row['owner_name'],
'photoUrl' => $row['owner_photo'] === null
? null
: client_cdn($row['owner_photo'], $team),
'id' => $row['owner_id'],
'job' => $row['owner_job'],
];
}
$result[(int) $row['opp_id']] = $data;
}
return $this->hydrateStages($result);
}
private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder
{
$qb = clone $queryBuilder;
$qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');
$qb
->select(...[
'opp.id as opp_id',
'opp.uuid',
'opp.name',
'opp.value',
'opp.currency_code',
'opp.close_date',
'opp.remotely_created_at',
'opp.is_closed',
'opp.is_won',
])
->addSelect(...[
'usr.uuid as owner_uuid',
'usr.name AS owner_name',
'usr.photo_path as owner_photo',
'usr.id AS owner_id',
'jt.name as owner_job',
])
->addSelect('opp.stage_id', 'opp.stage_updated_at')
->addSelect(...[
'acc.name AS acc_name',
'acc.is_internal as acc_is_internal',
'opp.stage_updated_at',
'acc.crm_provider_id AS acc_provider_id',
'opp.crm_provider_id AS opp_provider_id',
])
->addSelect('rt.business_process_id AS pipeline_id')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'));
return $qb;
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws SocialAccountTokenInvalidException
*/
private function getCrmService(Team $team): ServiceInterface
{
$crmService = $this->providerRegistry->get($team->crm->provider);
$crmService->setConfiguration($team->crm);
if ($crmService instanceof UrlGeneratorInterface) {
$crmService->setCrmUrlGenerator($team->crm);
}
return $crmService;
}
/**
*
* @return Generator<DealData>
*/
public function getForecastData(DealsFilter $filter): Generator
{
$opportunities = DB::query()
->select([
'o.value',
'o.close_date',
'o.currency_code',
'o.is_won',
'o.is_closed',
'o.probability',
'o.forecast_category',
])
->from('opportunities', 'o')
->join('users', 'users.id', '=', 'o.user_id')
->join('groups', 'groups.id', '=', 'users.group_id')
->where('users.team_id', $filter->getTeam()->getId())
->where('o.close_date', '>=', $filter->getStartDate())
->where('o.close_date', '<=', $filter->getEndDate())
->where('o.currency_code', $filter->getCurrency())
->where('o.deleted_at', '=', null)
;
$userUuidList = $filter->getUserUuidList();
if (! empty($userUuidList)) {
$userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);
$opportunities->whereIn('users.uuid', $userUuidList);
}
$groupUuidList = $filter->getGroupUuidList();
if (! empty($groupUuidList)) {
$groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);
$opportunities->whereIn('groups.uuid', $groupUuidList);
}
foreach ($opportunities->cursor() as $row) {
yield new DealData(
(float) $row->value,
$row->close_date,
! empty($row->is_won),
! empty($row->is_closed),
$row->probability ?: 0,
$row->forecast_category ?: '',
);
}
}
public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection
{
return $user->subscriptionSets()
->where(static function (Eloquent\Builder $query): void {
$query
->whereNull('expired_at')
->orWhere('expired_at', '>=', now());
})
->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {
$join
->on('subscription_set_id', '=', 'activity_subscription_sets.id');
$join
->where('followable_type', Models\Activity\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)
->whereIn('followable_id', $opportunityIds);
})
->pluck('followable_id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.25731382,"top":0.019952115,"width":0.03856383,"height":0.025538707},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20157-AJ-report-not-send-notification, menu","depth":5,"bounds":{"left":0.29587767,"top":0.019952115,"width":0.10139628,"height":0.025538707},"help_text":"Git Branch: JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.7972075,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"RequestGenerateAskJiminnyReportJobTest","depth":6,"bounds":{"left":0.8125,"top":0.019952115,"width":0.10305851,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"16","depth":4,"bounds":{"left":0.6030585,"top":0.17478053,"width":0.009640957,"height":0.015163607},"role_description":"text"},{"role":"AXStaticText","text":"7","depth":4,"bounds":{"left":0.6146942,"top":0.17478053,"width":0.0076462766,"height":0.015163607},"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.6243351,"top":0.17478053,"width":0.00731383,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.63331115,"top":0.17318435,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.640625,"top":0.17318435,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active Ask Jiminny reports whose expiry date has passed.\n *\n * @return Collection<AutomatedReport>\n */\n public function getExpiredActiveAskJiminnyReports(): Collection\n {\n return AutomatedReport::where('status', true)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereNotNull('expires_at')\n ->where('expires_at', '<', now()->toDateString())\n ->get();\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('report_id', $report->getId())\n ->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])\n ->latest()\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Restrict a query on the automated_reports table to reports the given user is allowed to see.\n *\n * Matches the customer-facing audience:\n * - explicit user recipients (recipients.users)\n * - members of any of the report's groups (Ask Jiminny reports)\n */\n private function applyUserAccessScope(Builder $query, User $user): void\n {\n $userId = $user->getId();\n $groupId = $user->getGroupId();\n\n $query\n ->where('automated_reports.team_id', $user->getTeamId())\n ->where(function (Builder $q) use ($userId, $groupId): void {\n $q->whereJsonContains('automated_reports.recipients->users', $userId);\n\n if ($groupId !== null) {\n $q->orWhere(function (Builder $sub) use ($groupId): void {\n $sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereJsonContains('automated_reports.groups', $groupId);\n });\n }\n });\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active Ask Jiminny reports whose expiry date has passed.\n *\n * @return Collection<AutomatedReport>\n */\n public function getExpiredActiveAskJiminnyReports(): Collection\n {\n return AutomatedReport::where('status', true)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereNotNull('expires_at')\n ->where('expires_at', '<', now()->toDateString())\n ->get();\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('report_id', $report->getId())\n ->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])\n ->latest()\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Restrict a query on the automated_reports table to reports the given user is allowed to see.\n *\n * Matches the customer-facing audience:\n * - explicit user recipients (recipients.users)\n * - members of any of the report's groups (Ask Jiminny reports)\n */\n private function applyUserAccessScope(Builder $query, User $user): void\n {\n $userId = $user->getId();\n $groupId = $user->getGroupId();\n\n $query\n ->where('automated_reports.team_id', $user->getTeamId())\n ->where(function (Builder $q) use ($userId, $groupId): void {\n $q->whereJsonContains('automated_reports.recipients->users', $userId);\n\n if ($groupId !== null) {\n $q->orWhere(function (Builder $sub) use ($groupId): void {\n $sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereJsonContains('automated_reports.groups', $groupId);\n });\n }\n });\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"36","depth":4,"bounds":{"left":0.69980055,"top":0.2490024,"width":0.010305851,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7117686,"top":0.24740623,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7190825,"top":0.24740623,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\DealInsights;\n\nuse Doctrine\\DBAL\\Connection;\nuse Generator;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealData;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealsFilter;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\QueryBuilder;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\Visitor\\QueryBuilderVisitorInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Services\\Crm\\IntegrationApp\\DTO\\Utils\\UrlGeneratorInterface;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Eloquent;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass DealsRepository implements DealsRepositoryInterface\n{\n private Connection $connection;\n\n private ProviderRegistry $providerRegistry;\n\n /**\n * @var QueryBuilderVisitorInterface[]\n */\n private array $visitors = [];\n\n /**\n * @param QueryBuilderVisitorInterface[] $visitors\n */\n public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])\n {\n $this->connection = $connection;\n $this->providerRegistry = $crmProviderRegistry;\n\n foreach ($visitors as $visitor) {\n $this->visitors[$visitor->getIdentifier()] = $visitor;\n }\n }\n\n public function getDeals(CriteriaInterface $criteria): array\n {\n $context = $criteria->getContext();\n $team = $context->getTeam();\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n\n $this->visit($qb, $criteria);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getDeal(Team $team, int $id): array\n {\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n $qb->andWhere('opp.id = :id')->setParameter('id', $id);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')\n ->from('crm_fields', 'f')\n ->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')\n ->where('f.crm_configuration_id = :crm')\n ->andWhere('f.object_type = :type')\n ->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')\n ->orderBy('fd.object_id', 'ASC')\n ->addOrderBy('fd.updated_at', 'ASC')\n\n ->setParameter('type', Field::OBJECT_OPPORTUNITY)\n ->setParameter('crm', $crmId)\n ;\n\n if (! empty($crmFields)) {\n $fields = array_map(fn ($value): string => '\"' . $value . '\"', $crmFields);\n $qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');\n }\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAssociative();\n }\n\n public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('COALESCE(opp.currency_code, \"' . $defaultCurrency . '\") AS currency')\n ->addSelect('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ->groupBy('currency')\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getDealActivities(CriteriaInterface $criteria): array\n {\n $qb = Activity::with(['participants', 'user'])\n ->where('opportunity_id', $criteria->getOpportunityId())\n ->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())\n ->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())\n ->orderBy($criteria->getSortBy(), $criteria->getSortDirection())\n ;\n\n // Should we filter activities by criteria? It's intended to filter deals.\n\n return $qb->get()->all();\n }\n\n public function getStages(CriteriaInterface $criteria): array\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('id', 'label', 'sequence')\n ->from('stages', 's')\n ->where('crm_configuration_id = :crm_configuration_id')\n ->andWhere('type = :type')\n ->orderBy('sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())\n ->setParameter('type', Stage::TYPE_OPPORTUNITY);\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $result[$row['id']] = [\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n public function getConfigurationStages(Configuration $configuration): Collection\n {\n return $configuration\n ->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get();\n }\n\n public function getPipelineData(Configuration $crm): array\n {\n $qb = new QueryBuilder($this->connection);\n $provider = $crm->provider;\n\n $qb\n ->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')\n ->from('stages', 's')\n ->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')\n ->where('s.crm_configuration_id = :crm_configuration_id')\n ->andWhere('s.type = :type')\n ->orderBy('bps.business_process_id', 'ASC')\n ->addOrderBy('s.sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $crm->id)\n ->setParameter('type', Stage::TYPE_OPPORTUNITY)\n ;\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];\n $result[$row['pipeline_id']][] = [\n 'value' => $value,\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n private function createQueryBuilder(string $realm): QueryBuilder\n {\n return (new QueryBuilder($this->connection))\n ->setRealm($realm)\n ->from('opportunities', 'opp')\n ->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')\n ->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')\n ->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')\n ;\n }\n\n /**\n * Applies all applicable visitors and returns the IDs of the executed ones\n *\n * @return string[]\n */\n private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array\n {\n $queryVisitors = [];\n\n foreach ($this->visitors as $visitor) {\n if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {\n $visitor->visit($queryBuilder, $criteria);\n\n $queryVisitors[] = $visitor->getIdentifier();\n }\n }\n\n return $queryVisitors;\n }\n\n private function hydrateStages(array $deals): array\n {\n foreach ($this->fetchStages(array_keys($deals)) as $stage) {\n $oppId = (int) $stage['opportunity_id'];\n\n if (! isset($deals[$oppId])) {\n continue; // or throw??!\n }\n\n $deals[$oppId]['stages'][] = [\n 'id' => $stage['stage_id'],\n 'name' => $stage['label'],\n 'enteredAt' => $stage['created_at'],\n ];\n }\n\n return $deals;\n }\n\n /**\n * @param int[] $dealIds\n */\n private function fetchStages(array $dealIds): array\n {\n if (empty($dealIds)) {\n return [];\n }\n\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')\n ->from('opportunity_stages', 'os')\n ->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')\n ->where($qb->expr()->in('os.opportunity_id', $dealIds))\n ->orderBy('os.opportunity_id', 'ASC')\n ->addOrderBy('s.created_at', 'ASC')\n ;\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array\n {\n $result = [];\n\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $data = [\n 'uuid' => RequiresUUID::toNormal($row['uuid']),\n 'name' => $row['name'],\n 'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),\n 'account' => [\n 'name' => $row['acc_name'],\n 'url' => $crmService->generateProviderUrl(\n providerId: $row['acc_provider_id'],\n objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'\n ),\n ],\n 'owner' => null,\n 'rawValue' => [\n 'amount' => (float) $row['value'],\n 'currency' => $row['currency_code'],\n ],\n 'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),\n 'openDate' => $row['remotely_created_at'] ?? null,\n 'closeDate' => $row['close_date'] ?? null,\n 'stages' => [],\n 'currentPipelineId' => $row['pipeline_id'],\n 'currentStage' => [\n 'id' => $row['stage_id'],\n 'enteredAt' => $row['stage_updated_at'],\n ],\n 'currentStageUpdatedAt' => $row['stage_updated_at'],\n 'isClosed' => (bool) $row['is_closed'],\n 'isWon' => (bool) $row['is_won'],\n ];\n\n if (isset($row['owner_uuid'])) {\n $data['owner'] = [\n 'uuid' => RequiresUUID::toNormal($row['owner_uuid']),\n 'name' => $row['owner_name'],\n 'photoUrl' => $row['owner_photo'] === null\n ? null\n : client_cdn($row['owner_photo'], $team),\n 'id' => $row['owner_id'],\n 'job' => $row['owner_job'],\n ];\n }\n\n $result[(int) $row['opp_id']] = $data;\n }\n\n return $this->hydrateStages($result);\n }\n\n private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder\n {\n $qb = clone $queryBuilder;\n $qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');\n\n $qb\n ->select(...[\n 'opp.id as opp_id',\n 'opp.uuid',\n 'opp.name',\n 'opp.value',\n 'opp.currency_code',\n 'opp.close_date',\n 'opp.remotely_created_at',\n 'opp.is_closed',\n 'opp.is_won',\n ])\n ->addSelect(...[\n 'usr.uuid as owner_uuid',\n 'usr.name AS owner_name',\n 'usr.photo_path as owner_photo',\n 'usr.id AS owner_id',\n 'jt.name as owner_job',\n ])\n ->addSelect('opp.stage_id', 'opp.stage_updated_at')\n ->addSelect(...[\n 'acc.name AS acc_name',\n 'acc.is_internal as acc_is_internal',\n 'opp.stage_updated_at',\n 'acc.crm_provider_id AS acc_provider_id',\n 'opp.crm_provider_id AS opp_provider_id',\n ])\n ->addSelect('rt.business_process_id AS pipeline_id')\n\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'));\n\n return $qb;\n }\n\n /**\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws SocialAccountTokenInvalidException\n */\n private function getCrmService(Team $team): ServiceInterface\n {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n $crmService->setConfiguration($team->crm);\n if ($crmService instanceof UrlGeneratorInterface) {\n $crmService->setCrmUrlGenerator($team->crm);\n }\n\n return $crmService;\n }\n\n /**\n *\n * @return Generator<DealData>\n */\n public function getForecastData(DealsFilter $filter): Generator\n {\n $opportunities = DB::query()\n ->select([\n 'o.value',\n 'o.close_date',\n 'o.currency_code',\n 'o.is_won',\n 'o.is_closed',\n 'o.probability',\n 'o.forecast_category',\n ])\n ->from('opportunities', 'o')\n ->join('users', 'users.id', '=', 'o.user_id')\n ->join('groups', 'groups.id', '=', 'users.group_id')\n ->where('users.team_id', $filter->getTeam()->getId())\n ->where('o.close_date', '>=', $filter->getStartDate())\n ->where('o.close_date', '<=', $filter->getEndDate())\n ->where('o.currency_code', $filter->getCurrency())\n ->where('o.deleted_at', '=', null)\n ;\n\n $userUuidList = $filter->getUserUuidList();\n if (! empty($userUuidList)) {\n $userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);\n\n $opportunities->whereIn('users.uuid', $userUuidList);\n }\n\n $groupUuidList = $filter->getGroupUuidList();\n if (! empty($groupUuidList)) {\n $groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);\n\n $opportunities->whereIn('groups.uuid', $groupUuidList);\n }\n\n foreach ($opportunities->cursor() as $row) {\n yield new DealData(\n (float) $row->value,\n $row->close_date,\n ! empty($row->is_won),\n ! empty($row->is_closed),\n $row->probability ?: 0,\n $row->forecast_category ?: '',\n );\n }\n }\n\n public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection\n {\n return $user->subscriptionSets()\n ->where(static function (Eloquent\\Builder $query): void {\n $query\n ->whereNull('expired_at')\n ->orWhere('expired_at', '>=', now());\n })\n ->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n $join\n ->where('followable_type', Models\\Activity\\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)\n ->whereIn('followable_id', $opportunityIds);\n })\n ->pluck('followable_id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\DealInsights;\n\nuse Doctrine\\DBAL\\Connection;\nuse Generator;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealData;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealsFilter;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\QueryBuilder;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\Visitor\\QueryBuilderVisitorInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Services\\Crm\\IntegrationApp\\DTO\\Utils\\UrlGeneratorInterface;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Eloquent;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass DealsRepository implements DealsRepositoryInterface\n{\n private Connection $connection;\n\n private ProviderRegistry $providerRegistry;\n\n /**\n * @var QueryBuilderVisitorInterface[]\n */\n private array $visitors = [];\n\n /**\n * @param QueryBuilderVisitorInterface[] $visitors\n */\n public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])\n {\n $this->connection = $connection;\n $this->providerRegistry = $crmProviderRegistry;\n\n foreach ($visitors as $visitor) {\n $this->visitors[$visitor->getIdentifier()] = $visitor;\n }\n }\n\n public function getDeals(CriteriaInterface $criteria): array\n {\n $context = $criteria->getContext();\n $team = $context->getTeam();\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n\n $this->visit($qb, $criteria);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getDeal(Team $team, int $id): array\n {\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n $qb->andWhere('opp.id = :id')->setParameter('id', $id);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')\n ->from('crm_fields', 'f')\n ->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')\n ->where('f.crm_configuration_id = :crm')\n ->andWhere('f.object_type = :type')\n ->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')\n ->orderBy('fd.object_id', 'ASC')\n ->addOrderBy('fd.updated_at', 'ASC')\n\n ->setParameter('type', Field::OBJECT_OPPORTUNITY)\n ->setParameter('crm', $crmId)\n ;\n\n if (! empty($crmFields)) {\n $fields = array_map(fn ($value): string => '\"' . $value . '\"', $crmFields);\n $qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');\n }\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAssociative();\n }\n\n public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('COALESCE(opp.currency_code, \"' . $defaultCurrency . '\") AS currency')\n ->addSelect('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ->groupBy('currency')\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getDealActivities(CriteriaInterface $criteria): array\n {\n $qb = Activity::with(['participants', 'user'])\n ->where('opportunity_id', $criteria->getOpportunityId())\n ->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())\n ->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())\n ->orderBy($criteria->getSortBy(), $criteria->getSortDirection())\n ;\n\n // Should we filter activities by criteria? It's intended to filter deals.\n\n return $qb->get()->all();\n }\n\n public function getStages(CriteriaInterface $criteria): array\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('id', 'label', 'sequence')\n ->from('stages', 's')\n ->where('crm_configuration_id = :crm_configuration_id')\n ->andWhere('type = :type')\n ->orderBy('sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())\n ->setParameter('type', Stage::TYPE_OPPORTUNITY);\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $result[$row['id']] = [\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n public function getConfigurationStages(Configuration $configuration): Collection\n {\n return $configuration\n ->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get();\n }\n\n public function getPipelineData(Configuration $crm): array\n {\n $qb = new QueryBuilder($this->connection);\n $provider = $crm->provider;\n\n $qb\n ->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')\n ->from('stages', 's')\n ->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')\n ->where('s.crm_configuration_id = :crm_configuration_id')\n ->andWhere('s.type = :type')\n ->orderBy('bps.business_process_id', 'ASC')\n ->addOrderBy('s.sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $crm->id)\n ->setParameter('type', Stage::TYPE_OPPORTUNITY)\n ;\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];\n $result[$row['pipeline_id']][] = [\n 'value' => $value,\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n private function createQueryBuilder(string $realm): QueryBuilder\n {\n return (new QueryBuilder($this->connection))\n ->setRealm($realm)\n ->from('opportunities', 'opp')\n ->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')\n ->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')\n ->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')\n ;\n }\n\n /**\n * Applies all applicable visitors and returns the IDs of the executed ones\n *\n * @return string[]\n */\n private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array\n {\n $queryVisitors = [];\n\n foreach ($this->visitors as $visitor) {\n if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {\n $visitor->visit($queryBuilder, $criteria);\n\n $queryVisitors[] = $visitor->getIdentifier();\n }\n }\n\n return $queryVisitors;\n }\n\n private function hydrateStages(array $deals): array\n {\n foreach ($this->fetchStages(array_keys($deals)) as $stage) {\n $oppId = (int) $stage['opportunity_id'];\n\n if (! isset($deals[$oppId])) {\n continue; // or throw??!\n }\n\n $deals[$oppId]['stages'][] = [\n 'id' => $stage['stage_id'],\n 'name' => $stage['label'],\n 'enteredAt' => $stage['created_at'],\n ];\n }\n\n return $deals;\n }\n\n /**\n * @param int[] $dealIds\n */\n private function fetchStages(array $dealIds): array\n {\n if (empty($dealIds)) {\n return [];\n }\n\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')\n ->from('opportunity_stages', 'os')\n ->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')\n ->where($qb->expr()->in('os.opportunity_id', $dealIds))\n ->orderBy('os.opportunity_id', 'ASC')\n ->addOrderBy('s.created_at', 'ASC')\n ;\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array\n {\n $result = [];\n\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $data = [\n 'uuid' => RequiresUUID::toNormal($row['uuid']),\n 'name' => $row['name'],\n 'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),\n 'account' => [\n 'name' => $row['acc_name'],\n 'url' => $crmService->generateProviderUrl(\n providerId: $row['acc_provider_id'],\n objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'\n ),\n ],\n 'owner' => null,\n 'rawValue' => [\n 'amount' => (float) $row['value'],\n 'currency' => $row['currency_code'],\n ],\n 'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),\n 'openDate' => $row['remotely_created_at'] ?? null,\n 'closeDate' => $row['close_date'] ?? null,\n 'stages' => [],\n 'currentPipelineId' => $row['pipeline_id'],\n 'currentStage' => [\n 'id' => $row['stage_id'],\n 'enteredAt' => $row['stage_updated_at'],\n ],\n 'currentStageUpdatedAt' => $row['stage_updated_at'],\n 'isClosed' => (bool) $row['is_closed'],\n 'isWon' => (bool) $row['is_won'],\n ];\n\n if (isset($row['owner_uuid'])) {\n $data['owner'] = [\n 'uuid' => RequiresUUID::toNormal($row['owner_uuid']),\n 'name' => $row['owner_name'],\n 'photoUrl' => $row['owner_photo'] === null\n ? null\n : client_cdn($row['owner_photo'], $team),\n 'id' => $row['owner_id'],\n 'job' => $row['owner_job'],\n ];\n }\n\n $result[(int) $row['opp_id']] = $data;\n }\n\n return $this->hydrateStages($result);\n }\n\n private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder\n {\n $qb = clone $queryBuilder;\n $qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');\n\n $qb\n ->select(...[\n 'opp.id as opp_id',\n 'opp.uuid',\n 'opp.name',\n 'opp.value',\n 'opp.currency_code',\n 'opp.close_date',\n 'opp.remotely_created_at',\n 'opp.is_closed',\n 'opp.is_won',\n ])\n ->addSelect(...[\n 'usr.uuid as owner_uuid',\n 'usr.name AS owner_name',\n 'usr.photo_path as owner_photo',\n 'usr.id AS owner_id',\n 'jt.name as owner_job',\n ])\n ->addSelect('opp.stage_id', 'opp.stage_updated_at')\n ->addSelect(...[\n 'acc.name AS acc_name',\n 'acc.is_internal as acc_is_internal',\n 'opp.stage_updated_at',\n 'acc.crm_provider_id AS acc_provider_id',\n 'opp.crm_provider_id AS opp_provider_id',\n ])\n ->addSelect('rt.business_process_id AS pipeline_id')\n\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'));\n\n return $qb;\n }\n\n /**\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws SocialAccountTokenInvalidException\n */\n private function getCrmService(Team $team): ServiceInterface\n {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n $crmService->setConfiguration($team->crm);\n if ($crmService instanceof UrlGeneratorInterface) {\n $crmService->setCrmUrlGenerator($team->crm);\n }\n\n return $crmService;\n }\n\n /**\n *\n * @return Generator<DealData>\n */\n public function getForecastData(DealsFilter $filter): Generator\n {\n $opportunities = DB::query()\n ->select([\n 'o.value',\n 'o.close_date',\n 'o.currency_code',\n 'o.is_won',\n 'o.is_closed',\n 'o.probability',\n 'o.forecast_category',\n ])\n ->from('opportunities', 'o')\n ->join('users', 'users.id', '=', 'o.user_id')\n ->join('groups', 'groups.id', '=', 'users.group_id')\n ->where('users.team_id', $filter->getTeam()->getId())\n ->where('o.close_date', '>=', $filter->getStartDate())\n ->where('o.close_date', '<=', $filter->getEndDate())\n ->where('o.currency_code', $filter->getCurrency())\n ->where('o.deleted_at', '=', null)\n ;\n\n $userUuidList = $filter->getUserUuidList();\n if (! empty($userUuidList)) {\n $userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);\n\n $opportunities->whereIn('users.uuid', $userUuidList);\n }\n\n $groupUuidList = $filter->getGroupUuidList();\n if (! empty($groupUuidList)) {\n $groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);\n\n $opportunities->whereIn('groups.uuid', $groupUuidList);\n }\n\n foreach ($opportunities->cursor() as $row) {\n yield new DealData(\n (float) $row->value,\n $row->close_date,\n ! empty($row->is_won),\n ! empty($row->is_closed),\n $row->probability ?: 0,\n $row->forecast_category ?: '',\n );\n }\n }\n\n public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection\n {\n return $user->subscriptionSets()\n ->where(static function (Eloquent\\Builder $query): void {\n $query\n ->whereNull('expired_at')\n ->orWhere('expired_at', '>=', now());\n })\n ->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n $join\n ->where('followable_type', Models\\Activity\\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)\n ->whereIn('followable_id', $opportunityIds);\n })\n ->pluck('followable_id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.24335106,"top":0.047885075,"width":0.024268618,"height":0.024740623},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3407362332434637772
|
-4167466460282672524
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
16
7
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active Ask Jiminny reports whose expiry date has passed.
*
* @return Collection<AutomatedReport>
*/
public function getExpiredActiveAskJiminnyReports(): Collection
{
return AutomatedReport::where('status', true)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereNotNull('expires_at')
->where('expires_at', '<', now()->toDateString())
->get();
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('report_id', $report->getId())
->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])
->latest()
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Restrict a query on the automated_reports table to reports the given user is allowed to see.
*
* Matches the customer-facing audience:
* - explicit user recipients (recipients.users)
* - members of any of the report's groups (Ask Jiminny reports)
*/
private function applyUserAccessScope(Builder $query, User $user): void
{
$userId = $user->getId();
$groupId = $user->getGroupId();
$query
->where('automated_reports.team_id', $user->getTeamId())
->where(function (Builder $q) use ($userId, $groupId): void {
$q->whereJsonContains('automated_reports.recipients->users', $userId);
if ($groupId !== null) {
$q->orWhere(function (Builder $sub) use ($groupId): void {
$sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereJsonContains('automated_reports.groups', $groupId);
});
}
});
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
36
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\DealInsights;
use Doctrine\DBAL\Connection;
use Generator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Jiminny\Component\DealInsights\Forecast\DealData;
use Jiminny\Component\DealInsights\Forecast\DealsFilter;
use Jiminny\Component\DealInsights\QueryBuilder\QueryBuilder;
use Jiminny\Component\DealInsights\QueryBuilder\Visitor\QueryBuilderVisitorInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Services\Crm\IntegrationApp\DTO\Utils\UrlGeneratorInterface;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class DealsRepository implements DealsRepositoryInterface
{
private Connection $connection;
private ProviderRegistry $providerRegistry;
/**
* @var QueryBuilderVisitorInterface[]
*/
private array $visitors = [];
/**
* @param QueryBuilderVisitorInterface[] $visitors
*/
public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])
{
$this->connection = $connection;
$this->providerRegistry = $crmProviderRegistry;
foreach ($visitors as $visitor) {
$this->visitors[$visitor->getIdentifier()] = $visitor;
}
}
public function getDeals(CriteriaInterface $criteria): array
{
$context = $criteria->getContext();
$team = $context->getTeam();
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$this->visit($qb, $criteria);
return $this->execute($team, $crmService, $qb);
}
public function getDeal(Team $team, int $id): array
{
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$qb->andWhere('opp.id = :id')->setParameter('id', $id);
return $this->execute($team, $crmService, $qb);
}
public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])
{
$qb = new QueryBuilder($this->connection);
$qb
->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')
->from('crm_fields', 'f')
->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')
->where('f.crm_configuration_id = :crm')
->andWhere('f.object_type = :type')
->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')
->orderBy('fd.object_id', 'ASC')
->addOrderBy('fd.updated_at', 'ASC')
->setParameter('type', Field::OBJECT_OPPORTUNITY)
->setParameter('crm', $crmId)
;
if (! empty($crmFields)) {
$fields = array_map(fn ($value): string => '"' . $value . '"', $crmFields);
$qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');
}
return $qb->executeQuery()->fetchAllAssociative();
}
public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAssociative();
}
public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('COALESCE(opp.currency_code, "' . $defaultCurrency . '") AS currency')
->addSelect('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
->groupBy('currency')
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAllAssociative();
}
public function getDealActivities(CriteriaInterface $criteria): array
{
$qb = Activity::with(['participants', 'user'])
->where('opportunity_id', $criteria->getOpportunityId())
->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())
->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())
->orderBy($criteria->getSortBy(), $criteria->getSortDirection())
;
// Should we filter activities by criteria? It's intended to filter deals.
return $qb->get()->all();
}
public function getStages(CriteriaInterface $criteria): array
{
$qb = new QueryBuilder($this->connection);
$qb
->select('id', 'label', 'sequence')
->from('stages', 's')
->where('crm_configuration_id = :crm_configuration_id')
->andWhere('type = :type')
->orderBy('sequence', 'ASC')
->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())
->setParameter('type', Stage::TYPE_OPPORTUNITY);
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$result[$row['id']] = [
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
public function getConfigurationStages(Configuration $configuration): Collection
{
return $configuration
->stages()
->where('type', Stage::TYPE_OPPORTUNITY)
->get();
}
public function getPipelineData(Configuration $crm): array
{
$qb = new QueryBuilder($this->connection);
$provider = $crm->provider;
$qb
->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')
->from('stages', 's')
->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')
->where('s.crm_configuration_id = :crm_configuration_id')
->andWhere('s.type = :type')
->orderBy('bps.business_process_id', 'ASC')
->addOrderBy('s.sequence', 'ASC')
->setParameter('crm_configuration_id', $crm->id)
->setParameter('type', Stage::TYPE_OPPORTUNITY)
;
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];
$result[$row['pipeline_id']][] = [
'value' => $value,
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
private function createQueryBuilder(string $realm): QueryBuilder
{
return (new QueryBuilder($this->connection))
->setRealm($realm)
->from('opportunities', 'opp')
->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')
->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')
->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')
;
}
/**
* Applies all applicable visitors and returns the IDs of the executed ones
*
* @return string[]
*/
private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array
{
$queryVisitors = [];
foreach ($this->visitors as $visitor) {
if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {
$visitor->visit($queryBuilder, $criteria);
$queryVisitors[] = $visitor->getIdentifier();
}
}
return $queryVisitors;
}
private function hydrateStages(array $deals): array
{
foreach ($this->fetchStages(array_keys($deals)) as $stage) {
$oppId = (int) $stage['opportunity_id'];
if (! isset($deals[$oppId])) {
continue; // or throw??!
}
$deals[$oppId]['stages'][] = [
'id' => $stage['stage_id'],
'name' => $stage['label'],
'enteredAt' => $stage['created_at'],
];
}
return $deals;
}
/**
* @param int[] $dealIds
*/
private function fetchStages(array $dealIds): array
{
if (empty($dealIds)) {
return [];
}
$qb = new QueryBuilder($this->connection);
$qb
->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')
->from('opportunity_stages', 'os')
->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')
->where($qb->expr()->in('os.opportunity_id', $dealIds))
->orderBy('os.opportunity_id', 'ASC')
->addOrderBy('s.created_at', 'ASC')
;
return $qb->executeQuery()->fetchAllAssociative();
}
private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array
{
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$data = [
'uuid' => RequiresUUID::toNormal($row['uuid']),
'name' => $row['name'],
'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),
'account' => [
'name' => $row['acc_name'],
'url' => $crmService->generateProviderUrl(
providerId: $row['acc_provider_id'],
objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'
),
],
'owner' => null,
'rawValue' => [
'amount' => (float) $row['value'],
'currency' => $row['currency_code'],
],
'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),
'openDate' => $row['remotely_created_at'] ?? null,
'closeDate' => $row['close_date'] ?? null,
'stages' => [],
'currentPipelineId' => $row['pipeline_id'],
'currentStage' => [
'id' => $row['stage_id'],
'enteredAt' => $row['stage_updated_at'],
],
'currentStageUpdatedAt' => $row['stage_updated_at'],
'isClosed' => (bool) $row['is_closed'],
'isWon' => (bool) $row['is_won'],
];
if (isset($row['owner_uuid'])) {
$data['owner'] = [
'uuid' => RequiresUUID::toNormal($row['owner_uuid']),
'name' => $row['owner_name'],
'photoUrl' => $row['owner_photo'] === null
? null
: client_cdn($row['owner_photo'], $team),
'id' => $row['owner_id'],
'job' => $row['owner_job'],
];
}
$result[(int) $row['opp_id']] = $data;
}
return $this->hydrateStages($result);
}
private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder
{
$qb = clone $queryBuilder;
$qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');
$qb
->select(...[
'opp.id as opp_id',
'opp.uuid',
'opp.name',
'opp.value',
'opp.currency_code',
'opp.close_date',
'opp.remotely_created_at',
'opp.is_closed',
'opp.is_won',
])
->addSelect(...[
'usr.uuid as owner_uuid',
'usr.name AS owner_name',
'usr.photo_path as owner_photo',
'usr.id AS owner_id',
'jt.name as owner_job',
])
->addSelect('opp.stage_id', 'opp.stage_updated_at')
->addSelect(...[
'acc.name AS acc_name',
'acc.is_internal as acc_is_internal',
'opp.stage_updated_at',
'acc.crm_provider_id AS acc_provider_id',
'opp.crm_provider_id AS opp_provider_id',
])
->addSelect('rt.business_process_id AS pipeline_id')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'));
return $qb;
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws SocialAccountTokenInvalidException
*/
private function getCrmService(Team $team): ServiceInterface
{
$crmService = $this->providerRegistry->get($team->crm->provider);
$crmService->setConfiguration($team->crm);
if ($crmService instanceof UrlGeneratorInterface) {
$crmService->setCrmUrlGenerator($team->crm);
}
return $crmService;
}
/**
*
* @return Generator<DealData>
*/
public function getForecastData(DealsFilter $filter): Generator
{
$opportunities = DB::query()
->select([
'o.value',
'o.close_date',
'o.currency_code',
'o.is_won',
'o.is_closed',
'o.probability',
'o.forecast_category',
])
->from('opportunities', 'o')
->join('users', 'users.id', '=', 'o.user_id')
->join('groups', 'groups.id', '=', 'users.group_id')
->where('users.team_id', $filter->getTeam()->getId())
->where('o.close_date', '>=', $filter->getStartDate())
->where('o.close_date', '<=', $filter->getEndDate())
->where('o.currency_code', $filter->getCurrency())
->where('o.deleted_at', '=', null)
;
$userUuidList = $filter->getUserUuidList();
if (! empty($userUuidList)) {
$userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);
$opportunities->whereIn('users.uuid', $userUuidList);
}
$groupUuidList = $filter->getGroupUuidList();
if (! empty($groupUuidList)) {
$groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);
$opportunities->whereIn('groups.uuid', $groupUuidList);
}
foreach ($opportunities->cursor() as $row) {
yield new DealData(
(float) $row->value,
$row->close_date,
! empty($row->is_won),
! empty($row->is_closed),
$row->probability ?: 0,
$row->forecast_category ?: '',
);
}
}
public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection
{
return $user->subscriptionSets()
->where(static function (Eloquent\Builder $query): void {
$query
->whereNull('expired_at')
->orWhere('expired_at', '>=', now());
})
->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {
$join
->on('subscription_set_id', '=', 'activity_subscription_sets.id');
$join
->where('followable_type', Models\Activity\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)
->whereIn('followable_id', $opportunityIds);
})
->pluck('followable_id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
72983
|
|
72985
|
NULL
|
0
|
2026-04-23T06:29:20.627009+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776925760627_m1.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsRepository.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
16
7
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active Ask Jiminny reports whose expiry date has passed.
*
* @return Collection<AutomatedReport>
*/
public function getExpiredActiveAskJiminnyReports(): Collection
{
return AutomatedReport::where('status', true)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereNotNull('expires_at')
->where('expires_at', '<', now()->toDateString())
->get();
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('report_id', $report->getId())
->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])
->latest()
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Restrict a query on the automated_reports table to reports the given user is allowed to see.
*
* Matches the customer-facing audience:
* - explicit user recipients (recipients.users)
* - members of any of the report's groups (Ask Jiminny reports)
*/
private function applyUserAccessScope(Builder $query, User $user): void
{
$userId = $user->getId();
$groupId = $user->getGroupId();
$query
->where('automated_reports.team_id', $user->getTeamId())
->where(function (Builder $q) use ($userId, $groupId): void {
$q->whereJsonContains('automated_reports.recipients->users', $userId);
if ($groupId !== null) {
$q->orWhere(function (Builder $sub) use ($groupId): void {
$sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereJsonContains('automated_reports.groups', $groupId);
});
}
});
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
36
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\DealInsights;
use Doctrine\DBAL\Connection;
use Generator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Jiminny\Component\DealInsights\Forecast\DealData;
use Jiminny\Component\DealInsights\Forecast\DealsFilter;
use Jiminny\Component\DealInsights\QueryBuilder\QueryBuilder;
use Jiminny\Component\DealInsights\QueryBuilder\Visitor\QueryBuilderVisitorInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Services\Crm\IntegrationApp\DTO\Utils\UrlGeneratorInterface;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class DealsRepository implements DealsRepositoryInterface
{
private Connection $connection;
private ProviderRegistry $providerRegistry;
/**
* @var QueryBuilderVisitorInterface[]
*/
private array $visitors = [];
/**
* @param QueryBuilderVisitorInterface[] $visitors
*/
public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])
{
$this->connection = $connection;
$this->providerRegistry = $crmProviderRegistry;
foreach ($visitors as $visitor) {
$this->visitors[$visitor->getIdentifier()] = $visitor;
}
}
public function getDeals(CriteriaInterface $criteria): array
{
$context = $criteria->getContext();
$team = $context->getTeam();
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$this->visit($qb, $criteria);
return $this->execute($team, $crmService, $qb);
}
public function getDeal(Team $team, int $id): array
{
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$qb->andWhere('opp.id = :id')->setParameter('id', $id);
return $this->execute($team, $crmService, $qb);
}
public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])
{
$qb = new QueryBuilder($this->connection);
$qb
->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')
->from('crm_fields', 'f')
->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')
->where('f.crm_configuration_id = :crm')
->andWhere('f.object_type = :type')
->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')
->orderBy('fd.object_id', 'ASC')
->addOrderBy('fd.updated_at', 'ASC')
->setParameter('type', Field::OBJECT_OPPORTUNITY)
->setParameter('crm', $crmId)
;
if (! empty($crmFields)) {
$fields = array_map(fn ($value): string => '"' . $value . '"', $crmFields);
$qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');
}
return $qb->executeQuery()->fetchAllAssociative();
}
public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAssociative();
}
public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('COALESCE(opp.currency_code, "' . $defaultCurrency . '") AS currency')
->addSelect('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
->groupBy('currency')
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAllAssociative();
}
public function getDealActivities(CriteriaInterface $criteria): array
{
$qb = Activity::with(['participants', 'user'])
->where('opportunity_id', $criteria->getOpportunityId())
->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())
->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())
->orderBy($criteria->getSortBy(), $criteria->getSortDirection())
;
// Should we filter activities by criteria? It's intended to filter deals.
return $qb->get()->all();
}
public function getStages(CriteriaInterface $criteria): array
{
$qb = new QueryBuilder($this->connection);
$qb
->select('id', 'label', 'sequence')
->from('stages', 's')
->where('crm_configuration_id = :crm_configuration_id')
->andWhere('type = :type')
->orderBy('sequence', 'ASC')
->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())
->setParameter('type', Stage::TYPE_OPPORTUNITY);
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$result[$row['id']] = [
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
public function getConfigurationStages(Configuration $configuration): Collection
{
return $configuration
->stages()
->where('type', Stage::TYPE_OPPORTUNITY)
->get();
}
public function getPipelineData(Configuration $crm): array
{
$qb = new QueryBuilder($this->connection);
$provider = $crm->provider;
$qb
->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')
->from('stages', 's')
->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')
->where('s.crm_configuration_id = :crm_configuration_id')
->andWhere('s.type = :type')
->orderBy('bps.business_process_id', 'ASC')
->addOrderBy('s.sequence', 'ASC')
->setParameter('crm_configuration_id', $crm->id)
->setParameter('type', Stage::TYPE_OPPORTUNITY)
;
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];
$result[$row['pipeline_id']][] = [
'value' => $value,
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
private function createQueryBuilder(string $realm): QueryBuilder
{
return (new QueryBuilder($this->connection))
->setRealm($realm)
->from('opportunities', 'opp')
->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')
->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')
->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')
;
}
/**
* Applies all applicable visitors and returns the IDs of the executed ones
*
* @return string[]
*/
private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array
{
$queryVisitors = [];
foreach ($this->visitors as $visitor) {
if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {
$visitor->visit($queryBuilder, $criteria);
$queryVisitors[] = $visitor->getIdentifier();
}
}
return $queryVisitors;
}
private function hydrateStages(array $deals): array
{
foreach ($this->fetchStages(array_keys($deals)) as $stage) {
$oppId = (int) $stage['opportunity_id'];
if (! isset($deals[$oppId])) {
continue; // or throw??!
}
$deals[$oppId]['stages'][] = [
'id' => $stage['stage_id'],
'name' => $stage['label'],
'enteredAt' => $stage['created_at'],
];
}
return $deals;
}
/**
* @param int[] $dealIds
*/
private function fetchStages(array $dealIds): array
{
if (empty($dealIds)) {
return [];
}
$qb = new QueryBuilder($this->connection);
$qb
->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')
->from('opportunity_stages', 'os')
->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')
->where($qb->expr()->in('os.opportunity_id', $dealIds))
->orderBy('os.opportunity_id', 'ASC')
->addOrderBy('s.created_at', 'ASC')
;
return $qb->executeQuery()->fetchAllAssociative();
}
private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array
{
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$data = [
'uuid' => RequiresUUID::toNormal($row['uuid']),
'name' => $row['name'],
'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),
'account' => [
'name' => $row['acc_name'],
'url' => $crmService->generateProviderUrl(
providerId: $row['acc_provider_id'],
objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'
),
],
'owner' => null,
'rawValue' => [
'amount' => (float) $row['value'],
'currency' => $row['currency_code'],
],
'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),
'openDate' => $row['remotely_created_at'] ?? null,
'closeDate' => $row['close_date'] ?? null,
'stages' => [],
'currentPipelineId' => $row['pipeline_id'],
'currentStage' => [
'id' => $row['stage_id'],
'enteredAt' => $row['stage_updated_at'],
],
'currentStageUpdatedAt' => $row['stage_updated_at'],
'isClosed' => (bool) $row['is_closed'],
'isWon' => (bool) $row['is_won'],
];
if (isset($row['owner_uuid'])) {
$data['owner'] = [
'uuid' => RequiresUUID::toNormal($row['owner_uuid']),
'name' => $row['owner_name'],
'photoUrl' => $row['owner_photo'] === null
? null
: client_cdn($row['owner_photo'], $team),
'id' => $row['owner_id'],
'job' => $row['owner_job'],
];
}
$result[(int) $row['opp_id']] = $data;
}
return $this->hydrateStages($result);
}
private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder
{
$qb = clone $queryBuilder;
$qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');
$qb
->select(...[
'opp.id as opp_id',
'opp.uuid',
'opp.name',
'opp.value',
'opp.currency_code',
'opp.close_date',
'opp.remotely_created_at',
'opp.is_closed',
'opp.is_won',
])
->addSelect(...[
'usr.uuid as owner_uuid',
'usr.name AS owner_name',
'usr.photo_path as owner_photo',
'usr.id AS owner_id',
'jt.name as owner_job',
])
->addSelect('opp.stage_id', 'opp.stage_updated_at')
->addSelect(...[
'acc.name AS acc_name',
'acc.is_internal as acc_is_internal',
'opp.stage_updated_at',
'acc.crm_provider_id AS acc_provider_id',
'opp.crm_provider_id AS opp_provider_id',
])
->addSelect('rt.business_process_id AS pipeline_id')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'));
return $qb;
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws SocialAccountTokenInvalidException
*/
private function getCrmService(Team $team): ServiceInterface
{
$crmService = $this->providerRegistry->get($team->crm->provider);
$crmService->setConfiguration($team->crm);
if ($crmService instanceof UrlGeneratorInterface) {
$crmService->setCrmUrlGenerator($team->crm);
}
return $crmService;
}
/**
*
* @return Generator<DealData>
*/
public function getForecastData(DealsFilter $filter): Generator
{
$opportunities = DB::query()
->select([
'o.value',
'o.close_date',
'o.currency_code',
'o.is_won',
'o.is_closed',
'o.probability',
'o.forecast_category',
])
->from('opportunities', 'o')
->join('users', 'users.id', '=', 'o.user_id')
->join('groups', 'groups.id', '=', 'users.group_id')
->where('users.team_id', $filter->getTeam()->getId())
->where('o.close_date', '>=', $filter->getStartDate())
->where('o.close_date', '<=', $filter->getEndDate())
->where('o.currency_code', $filter->getCurrency())
->where('o.deleted_at', '=', null)
;
$userUuidList = $filter->getUserUuidList();
if (! empty($userUuidList)) {
$userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);
$opportunities->whereIn('users.uuid', $userUuidList);
}
$groupUuidList = $filter->getGroupUuidList();
if (! empty($groupUuidList)) {
$groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);
$opportunities->whereIn('groups.uuid', $groupUuidList);
}
foreach ($opportunities->cursor() as $row) {
yield new DealData(
(float) $row->value,
$row->close_date,
! empty($row->is_won),
! empty($row->is_closed),
$row->probability ?: 0,
$row->forecast_category ?: '',
);
}
}
public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection
{
return $user->subscriptionSets()
->where(static function (Eloquent\Builder $query): void {
$query
->whereNull('expired_at')
->orWhere('expired_at', '>=', now());
})
->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {
$join
->on('subscription_set_id', '=', 'activity_subscription_sets.id');
$join
->where('followable_type', Models\Activity\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)
->whereIn('followable_id', $opportunityIds);
})
->pluck('followable_id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20157-AJ-report-not-send-notification, menu","depth":5,"help_text":"Git Branch: JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"RequestGenerateAskJiminnyReportJobTest","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"16","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"7","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active Ask Jiminny reports whose expiry date has passed.\n *\n * @return Collection<AutomatedReport>\n */\n public function getExpiredActiveAskJiminnyReports(): Collection\n {\n return AutomatedReport::where('status', true)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereNotNull('expires_at')\n ->where('expires_at', '<', now()->toDateString())\n ->get();\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('report_id', $report->getId())\n ->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])\n ->latest()\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Restrict a query on the automated_reports table to reports the given user is allowed to see.\n *\n * Matches the customer-facing audience:\n * - explicit user recipients (recipients.users)\n * - members of any of the report's groups (Ask Jiminny reports)\n */\n private function applyUserAccessScope(Builder $query, User $user): void\n {\n $userId = $user->getId();\n $groupId = $user->getGroupId();\n\n $query\n ->where('automated_reports.team_id', $user->getTeamId())\n ->where(function (Builder $q) use ($userId, $groupId): void {\n $q->whereJsonContains('automated_reports.recipients->users', $userId);\n\n if ($groupId !== null) {\n $q->orWhere(function (Builder $sub) use ($groupId): void {\n $sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereJsonContains('automated_reports.groups', $groupId);\n });\n }\n });\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active Ask Jiminny reports whose expiry date has passed.\n *\n * @return Collection<AutomatedReport>\n */\n public function getExpiredActiveAskJiminnyReports(): Collection\n {\n return AutomatedReport::where('status', true)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereNotNull('expires_at')\n ->where('expires_at', '<', now()->toDateString())\n ->get();\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('report_id', $report->getId())\n ->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])\n ->latest()\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Restrict a query on the automated_reports table to reports the given user is allowed to see.\n *\n * Matches the customer-facing audience:\n * - explicit user recipients (recipients.users)\n * - members of any of the report's groups (Ask Jiminny reports)\n */\n private function applyUserAccessScope(Builder $query, User $user): void\n {\n $userId = $user->getId();\n $groupId = $user->getGroupId();\n\n $query\n ->where('automated_reports.team_id', $user->getTeamId())\n ->where(function (Builder $q) use ($userId, $groupId): void {\n $q->whereJsonContains('automated_reports.recipients->users', $userId);\n\n if ($groupId !== null) {\n $q->orWhere(function (Builder $sub) use ($groupId): void {\n $sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereJsonContains('automated_reports.groups', $groupId);\n });\n }\n });\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"36","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\DealInsights;\n\nuse Doctrine\\DBAL\\Connection;\nuse Generator;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealData;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealsFilter;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\QueryBuilder;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\Visitor\\QueryBuilderVisitorInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Services\\Crm\\IntegrationApp\\DTO\\Utils\\UrlGeneratorInterface;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Eloquent;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass DealsRepository implements DealsRepositoryInterface\n{\n private Connection $connection;\n\n private ProviderRegistry $providerRegistry;\n\n /**\n * @var QueryBuilderVisitorInterface[]\n */\n private array $visitors = [];\n\n /**\n * @param QueryBuilderVisitorInterface[] $visitors\n */\n public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])\n {\n $this->connection = $connection;\n $this->providerRegistry = $crmProviderRegistry;\n\n foreach ($visitors as $visitor) {\n $this->visitors[$visitor->getIdentifier()] = $visitor;\n }\n }\n\n public function getDeals(CriteriaInterface $criteria): array\n {\n $context = $criteria->getContext();\n $team = $context->getTeam();\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n\n $this->visit($qb, $criteria);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getDeal(Team $team, int $id): array\n {\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n $qb->andWhere('opp.id = :id')->setParameter('id', $id);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')\n ->from('crm_fields', 'f')\n ->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')\n ->where('f.crm_configuration_id = :crm')\n ->andWhere('f.object_type = :type')\n ->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')\n ->orderBy('fd.object_id', 'ASC')\n ->addOrderBy('fd.updated_at', 'ASC')\n\n ->setParameter('type', Field::OBJECT_OPPORTUNITY)\n ->setParameter('crm', $crmId)\n ;\n\n if (! empty($crmFields)) {\n $fields = array_map(fn ($value): string => '\"' . $value . '\"', $crmFields);\n $qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');\n }\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAssociative();\n }\n\n public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('COALESCE(opp.currency_code, \"' . $defaultCurrency . '\") AS currency')\n ->addSelect('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ->groupBy('currency')\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getDealActivities(CriteriaInterface $criteria): array\n {\n $qb = Activity::with(['participants', 'user'])\n ->where('opportunity_id', $criteria->getOpportunityId())\n ->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())\n ->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())\n ->orderBy($criteria->getSortBy(), $criteria->getSortDirection())\n ;\n\n // Should we filter activities by criteria? It's intended to filter deals.\n\n return $qb->get()->all();\n }\n\n public function getStages(CriteriaInterface $criteria): array\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('id', 'label', 'sequence')\n ->from('stages', 's')\n ->where('crm_configuration_id = :crm_configuration_id')\n ->andWhere('type = :type')\n ->orderBy('sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())\n ->setParameter('type', Stage::TYPE_OPPORTUNITY);\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $result[$row['id']] = [\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n public function getConfigurationStages(Configuration $configuration): Collection\n {\n return $configuration\n ->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get();\n }\n\n public function getPipelineData(Configuration $crm): array\n {\n $qb = new QueryBuilder($this->connection);\n $provider = $crm->provider;\n\n $qb\n ->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')\n ->from('stages', 's')\n ->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')\n ->where('s.crm_configuration_id = :crm_configuration_id')\n ->andWhere('s.type = :type')\n ->orderBy('bps.business_process_id', 'ASC')\n ->addOrderBy('s.sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $crm->id)\n ->setParameter('type', Stage::TYPE_OPPORTUNITY)\n ;\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];\n $result[$row['pipeline_id']][] = [\n 'value' => $value,\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n private function createQueryBuilder(string $realm): QueryBuilder\n {\n return (new QueryBuilder($this->connection))\n ->setRealm($realm)\n ->from('opportunities', 'opp')\n ->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')\n ->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')\n ->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')\n ;\n }\n\n /**\n * Applies all applicable visitors and returns the IDs of the executed ones\n *\n * @return string[]\n */\n private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array\n {\n $queryVisitors = [];\n\n foreach ($this->visitors as $visitor) {\n if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {\n $visitor->visit($queryBuilder, $criteria);\n\n $queryVisitors[] = $visitor->getIdentifier();\n }\n }\n\n return $queryVisitors;\n }\n\n private function hydrateStages(array $deals): array\n {\n foreach ($this->fetchStages(array_keys($deals)) as $stage) {\n $oppId = (int) $stage['opportunity_id'];\n\n if (! isset($deals[$oppId])) {\n continue; // or throw??!\n }\n\n $deals[$oppId]['stages'][] = [\n 'id' => $stage['stage_id'],\n 'name' => $stage['label'],\n 'enteredAt' => $stage['created_at'],\n ];\n }\n\n return $deals;\n }\n\n /**\n * @param int[] $dealIds\n */\n private function fetchStages(array $dealIds): array\n {\n if (empty($dealIds)) {\n return [];\n }\n\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')\n ->from('opportunity_stages', 'os')\n ->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')\n ->where($qb->expr()->in('os.opportunity_id', $dealIds))\n ->orderBy('os.opportunity_id', 'ASC')\n ->addOrderBy('s.created_at', 'ASC')\n ;\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array\n {\n $result = [];\n\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $data = [\n 'uuid' => RequiresUUID::toNormal($row['uuid']),\n 'name' => $row['name'],\n 'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),\n 'account' => [\n 'name' => $row['acc_name'],\n 'url' => $crmService->generateProviderUrl(\n providerId: $row['acc_provider_id'],\n objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'\n ),\n ],\n 'owner' => null,\n 'rawValue' => [\n 'amount' => (float) $row['value'],\n 'currency' => $row['currency_code'],\n ],\n 'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),\n 'openDate' => $row['remotely_created_at'] ?? null,\n 'closeDate' => $row['close_date'] ?? null,\n 'stages' => [],\n 'currentPipelineId' => $row['pipeline_id'],\n 'currentStage' => [\n 'id' => $row['stage_id'],\n 'enteredAt' => $row['stage_updated_at'],\n ],\n 'currentStageUpdatedAt' => $row['stage_updated_at'],\n 'isClosed' => (bool) $row['is_closed'],\n 'isWon' => (bool) $row['is_won'],\n ];\n\n if (isset($row['owner_uuid'])) {\n $data['owner'] = [\n 'uuid' => RequiresUUID::toNormal($row['owner_uuid']),\n 'name' => $row['owner_name'],\n 'photoUrl' => $row['owner_photo'] === null\n ? null\n : client_cdn($row['owner_photo'], $team),\n 'id' => $row['owner_id'],\n 'job' => $row['owner_job'],\n ];\n }\n\n $result[(int) $row['opp_id']] = $data;\n }\n\n return $this->hydrateStages($result);\n }\n\n private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder\n {\n $qb = clone $queryBuilder;\n $qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');\n\n $qb\n ->select(...[\n 'opp.id as opp_id',\n 'opp.uuid',\n 'opp.name',\n 'opp.value',\n 'opp.currency_code',\n 'opp.close_date',\n 'opp.remotely_created_at',\n 'opp.is_closed',\n 'opp.is_won',\n ])\n ->addSelect(...[\n 'usr.uuid as owner_uuid',\n 'usr.name AS owner_name',\n 'usr.photo_path as owner_photo',\n 'usr.id AS owner_id',\n 'jt.name as owner_job',\n ])\n ->addSelect('opp.stage_id', 'opp.stage_updated_at')\n ->addSelect(...[\n 'acc.name AS acc_name',\n 'acc.is_internal as acc_is_internal',\n 'opp.stage_updated_at',\n 'acc.crm_provider_id AS acc_provider_id',\n 'opp.crm_provider_id AS opp_provider_id',\n ])\n ->addSelect('rt.business_process_id AS pipeline_id')\n\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'));\n\n return $qb;\n }\n\n /**\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws SocialAccountTokenInvalidException\n */\n private function getCrmService(Team $team): ServiceInterface\n {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n $crmService->setConfiguration($team->crm);\n if ($crmService instanceof UrlGeneratorInterface) {\n $crmService->setCrmUrlGenerator($team->crm);\n }\n\n return $crmService;\n }\n\n /**\n *\n * @return Generator<DealData>\n */\n public function getForecastData(DealsFilter $filter): Generator\n {\n $opportunities = DB::query()\n ->select([\n 'o.value',\n 'o.close_date',\n 'o.currency_code',\n 'o.is_won',\n 'o.is_closed',\n 'o.probability',\n 'o.forecast_category',\n ])\n ->from('opportunities', 'o')\n ->join('users', 'users.id', '=', 'o.user_id')\n ->join('groups', 'groups.id', '=', 'users.group_id')\n ->where('users.team_id', $filter->getTeam()->getId())\n ->where('o.close_date', '>=', $filter->getStartDate())\n ->where('o.close_date', '<=', $filter->getEndDate())\n ->where('o.currency_code', $filter->getCurrency())\n ->where('o.deleted_at', '=', null)\n ;\n\n $userUuidList = $filter->getUserUuidList();\n if (! empty($userUuidList)) {\n $userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);\n\n $opportunities->whereIn('users.uuid', $userUuidList);\n }\n\n $groupUuidList = $filter->getGroupUuidList();\n if (! empty($groupUuidList)) {\n $groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);\n\n $opportunities->whereIn('groups.uuid', $groupUuidList);\n }\n\n foreach ($opportunities->cursor() as $row) {\n yield new DealData(\n (float) $row->value,\n $row->close_date,\n ! empty($row->is_won),\n ! empty($row->is_closed),\n $row->probability ?: 0,\n $row->forecast_category ?: '',\n );\n }\n }\n\n public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection\n {\n return $user->subscriptionSets()\n ->where(static function (Eloquent\\Builder $query): void {\n $query\n ->whereNull('expired_at')\n ->orWhere('expired_at', '>=', now());\n })\n ->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n $join\n ->where('followable_type', Models\\Activity\\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)\n ->whereIn('followable_id', $opportunityIds);\n })\n ->pluck('followable_id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\DealInsights;\n\nuse Doctrine\\DBAL\\Connection;\nuse Generator;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealData;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealsFilter;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\QueryBuilder;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\Visitor\\QueryBuilderVisitorInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Services\\Crm\\IntegrationApp\\DTO\\Utils\\UrlGeneratorInterface;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Eloquent;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass DealsRepository implements DealsRepositoryInterface\n{\n private Connection $connection;\n\n private ProviderRegistry $providerRegistry;\n\n /**\n * @var QueryBuilderVisitorInterface[]\n */\n private array $visitors = [];\n\n /**\n * @param QueryBuilderVisitorInterface[] $visitors\n */\n public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])\n {\n $this->connection = $connection;\n $this->providerRegistry = $crmProviderRegistry;\n\n foreach ($visitors as $visitor) {\n $this->visitors[$visitor->getIdentifier()] = $visitor;\n }\n }\n\n public function getDeals(CriteriaInterface $criteria): array\n {\n $context = $criteria->getContext();\n $team = $context->getTeam();\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n\n $this->visit($qb, $criteria);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getDeal(Team $team, int $id): array\n {\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n $qb->andWhere('opp.id = :id')->setParameter('id', $id);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')\n ->from('crm_fields', 'f')\n ->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')\n ->where('f.crm_configuration_id = :crm')\n ->andWhere('f.object_type = :type')\n ->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')\n ->orderBy('fd.object_id', 'ASC')\n ->addOrderBy('fd.updated_at', 'ASC')\n\n ->setParameter('type', Field::OBJECT_OPPORTUNITY)\n ->setParameter('crm', $crmId)\n ;\n\n if (! empty($crmFields)) {\n $fields = array_map(fn ($value): string => '\"' . $value . '\"', $crmFields);\n $qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');\n }\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAssociative();\n }\n\n public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('COALESCE(opp.currency_code, \"' . $defaultCurrency . '\") AS currency')\n ->addSelect('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ->groupBy('currency')\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getDealActivities(CriteriaInterface $criteria): array\n {\n $qb = Activity::with(['participants', 'user'])\n ->where('opportunity_id', $criteria->getOpportunityId())\n ->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())\n ->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())\n ->orderBy($criteria->getSortBy(), $criteria->getSortDirection())\n ;\n\n // Should we filter activities by criteria? It's intended to filter deals.\n\n return $qb->get()->all();\n }\n\n public function getStages(CriteriaInterface $criteria): array\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('id', 'label', 'sequence')\n ->from('stages', 's')\n ->where('crm_configuration_id = :crm_configuration_id')\n ->andWhere('type = :type')\n ->orderBy('sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())\n ->setParameter('type', Stage::TYPE_OPPORTUNITY);\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $result[$row['id']] = [\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n public function getConfigurationStages(Configuration $configuration): Collection\n {\n return $configuration\n ->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get();\n }\n\n public function getPipelineData(Configuration $crm): array\n {\n $qb = new QueryBuilder($this->connection);\n $provider = $crm->provider;\n\n $qb\n ->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')\n ->from('stages', 's')\n ->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')\n ->where('s.crm_configuration_id = :crm_configuration_id')\n ->andWhere('s.type = :type')\n ->orderBy('bps.business_process_id', 'ASC')\n ->addOrderBy('s.sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $crm->id)\n ->setParameter('type', Stage::TYPE_OPPORTUNITY)\n ;\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];\n $result[$row['pipeline_id']][] = [\n 'value' => $value,\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n private function createQueryBuilder(string $realm): QueryBuilder\n {\n return (new QueryBuilder($this->connection))\n ->setRealm($realm)\n ->from('opportunities', 'opp')\n ->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')\n ->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')\n ->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')\n ;\n }\n\n /**\n * Applies all applicable visitors and returns the IDs of the executed ones\n *\n * @return string[]\n */\n private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array\n {\n $queryVisitors = [];\n\n foreach ($this->visitors as $visitor) {\n if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {\n $visitor->visit($queryBuilder, $criteria);\n\n $queryVisitors[] = $visitor->getIdentifier();\n }\n }\n\n return $queryVisitors;\n }\n\n private function hydrateStages(array $deals): array\n {\n foreach ($this->fetchStages(array_keys($deals)) as $stage) {\n $oppId = (int) $stage['opportunity_id'];\n\n if (! isset($deals[$oppId])) {\n continue; // or throw??!\n }\n\n $deals[$oppId]['stages'][] = [\n 'id' => $stage['stage_id'],\n 'name' => $stage['label'],\n 'enteredAt' => $stage['created_at'],\n ];\n }\n\n return $deals;\n }\n\n /**\n * @param int[] $dealIds\n */\n private function fetchStages(array $dealIds): array\n {\n if (empty($dealIds)) {\n return [];\n }\n\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')\n ->from('opportunity_stages', 'os')\n ->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')\n ->where($qb->expr()->in('os.opportunity_id', $dealIds))\n ->orderBy('os.opportunity_id', 'ASC')\n ->addOrderBy('s.created_at', 'ASC')\n ;\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array\n {\n $result = [];\n\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $data = [\n 'uuid' => RequiresUUID::toNormal($row['uuid']),\n 'name' => $row['name'],\n 'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),\n 'account' => [\n 'name' => $row['acc_name'],\n 'url' => $crmService->generateProviderUrl(\n providerId: $row['acc_provider_id'],\n objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'\n ),\n ],\n 'owner' => null,\n 'rawValue' => [\n 'amount' => (float) $row['value'],\n 'currency' => $row['currency_code'],\n ],\n 'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),\n 'openDate' => $row['remotely_created_at'] ?? null,\n 'closeDate' => $row['close_date'] ?? null,\n 'stages' => [],\n 'currentPipelineId' => $row['pipeline_id'],\n 'currentStage' => [\n 'id' => $row['stage_id'],\n 'enteredAt' => $row['stage_updated_at'],\n ],\n 'currentStageUpdatedAt' => $row['stage_updated_at'],\n 'isClosed' => (bool) $row['is_closed'],\n 'isWon' => (bool) $row['is_won'],\n ];\n\n if (isset($row['owner_uuid'])) {\n $data['owner'] = [\n 'uuid' => RequiresUUID::toNormal($row['owner_uuid']),\n 'name' => $row['owner_name'],\n 'photoUrl' => $row['owner_photo'] === null\n ? null\n : client_cdn($row['owner_photo'], $team),\n 'id' => $row['owner_id'],\n 'job' => $row['owner_job'],\n ];\n }\n\n $result[(int) $row['opp_id']] = $data;\n }\n\n return $this->hydrateStages($result);\n }\n\n private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder\n {\n $qb = clone $queryBuilder;\n $qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');\n\n $qb\n ->select(...[\n 'opp.id as opp_id',\n 'opp.uuid',\n 'opp.name',\n 'opp.value',\n 'opp.currency_code',\n 'opp.close_date',\n 'opp.remotely_created_at',\n 'opp.is_closed',\n 'opp.is_won',\n ])\n ->addSelect(...[\n 'usr.uuid as owner_uuid',\n 'usr.name AS owner_name',\n 'usr.photo_path as owner_photo',\n 'usr.id AS owner_id',\n 'jt.name as owner_job',\n ])\n ->addSelect('opp.stage_id', 'opp.stage_updated_at')\n ->addSelect(...[\n 'acc.name AS acc_name',\n 'acc.is_internal as acc_is_internal',\n 'opp.stage_updated_at',\n 'acc.crm_provider_id AS acc_provider_id',\n 'opp.crm_provider_id AS opp_provider_id',\n ])\n ->addSelect('rt.business_process_id AS pipeline_id')\n\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'));\n\n return $qb;\n }\n\n /**\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws SocialAccountTokenInvalidException\n */\n private function getCrmService(Team $team): ServiceInterface\n {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n $crmService->setConfiguration($team->crm);\n if ($crmService instanceof UrlGeneratorInterface) {\n $crmService->setCrmUrlGenerator($team->crm);\n }\n\n return $crmService;\n }\n\n /**\n *\n * @return Generator<DealData>\n */\n public function getForecastData(DealsFilter $filter): Generator\n {\n $opportunities = DB::query()\n ->select([\n 'o.value',\n 'o.close_date',\n 'o.currency_code',\n 'o.is_won',\n 'o.is_closed',\n 'o.probability',\n 'o.forecast_category',\n ])\n ->from('opportunities', 'o')\n ->join('users', 'users.id', '=', 'o.user_id')\n ->join('groups', 'groups.id', '=', 'users.group_id')\n ->where('users.team_id', $filter->getTeam()->getId())\n ->where('o.close_date', '>=', $filter->getStartDate())\n ->where('o.close_date', '<=', $filter->getEndDate())\n ->where('o.currency_code', $filter->getCurrency())\n ->where('o.deleted_at', '=', null)\n ;\n\n $userUuidList = $filter->getUserUuidList();\n if (! empty($userUuidList)) {\n $userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);\n\n $opportunities->whereIn('users.uuid', $userUuidList);\n }\n\n $groupUuidList = $filter->getGroupUuidList();\n if (! empty($groupUuidList)) {\n $groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);\n\n $opportunities->whereIn('groups.uuid', $groupUuidList);\n }\n\n foreach ($opportunities->cursor() as $row) {\n yield new DealData(\n (float) $row->value,\n $row->close_date,\n ! empty($row->is_won),\n ! empty($row->is_closed),\n $row->probability ?: 0,\n $row->forecast_category ?: '',\n );\n }\n }\n\n public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection\n {\n return $user->subscriptionSets()\n ->where(static function (Eloquent\\Builder $query): void {\n $query\n ->whereNull('expired_at')\n ->orWhere('expired_at', '>=', now());\n })\n ->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n $join\n ->where('followable_type', Models\\Activity\\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)\n ->whereIn('followable_id', $opportunityIds);\n })\n ->pluck('followable_id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3407362332434637772
|
-4167466460282672524
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
16
7
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active Ask Jiminny reports whose expiry date has passed.
*
* @return Collection<AutomatedReport>
*/
public function getExpiredActiveAskJiminnyReports(): Collection
{
return AutomatedReport::where('status', true)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereNotNull('expires_at')
->where('expires_at', '<', now()->toDateString())
->get();
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('report_id', $report->getId())
->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])
->latest()
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Restrict a query on the automated_reports table to reports the given user is allowed to see.
*
* Matches the customer-facing audience:
* - explicit user recipients (recipients.users)
* - members of any of the report's groups (Ask Jiminny reports)
*/
private function applyUserAccessScope(Builder $query, User $user): void
{
$userId = $user->getId();
$groupId = $user->getGroupId();
$query
->where('automated_reports.team_id', $user->getTeamId())
->where(function (Builder $q) use ($userId, $groupId): void {
$q->whereJsonContains('automated_reports.recipients->users', $userId);
if ($groupId !== null) {
$q->orWhere(function (Builder $sub) use ($groupId): void {
$sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereJsonContains('automated_reports.groups', $groupId);
});
}
});
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
36
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\DealInsights;
use Doctrine\DBAL\Connection;
use Generator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Jiminny\Component\DealInsights\Forecast\DealData;
use Jiminny\Component\DealInsights\Forecast\DealsFilter;
use Jiminny\Component\DealInsights\QueryBuilder\QueryBuilder;
use Jiminny\Component\DealInsights\QueryBuilder\Visitor\QueryBuilderVisitorInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Services\Crm\IntegrationApp\DTO\Utils\UrlGeneratorInterface;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class DealsRepository implements DealsRepositoryInterface
{
private Connection $connection;
private ProviderRegistry $providerRegistry;
/**
* @var QueryBuilderVisitorInterface[]
*/
private array $visitors = [];
/**
* @param QueryBuilderVisitorInterface[] $visitors
*/
public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])
{
$this->connection = $connection;
$this->providerRegistry = $crmProviderRegistry;
foreach ($visitors as $visitor) {
$this->visitors[$visitor->getIdentifier()] = $visitor;
}
}
public function getDeals(CriteriaInterface $criteria): array
{
$context = $criteria->getContext();
$team = $context->getTeam();
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$this->visit($qb, $criteria);
return $this->execute($team, $crmService, $qb);
}
public function getDeal(Team $team, int $id): array
{
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$qb->andWhere('opp.id = :id')->setParameter('id', $id);
return $this->execute($team, $crmService, $qb);
}
public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])
{
$qb = new QueryBuilder($this->connection);
$qb
->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')
->from('crm_fields', 'f')
->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')
->where('f.crm_configuration_id = :crm')
->andWhere('f.object_type = :type')
->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')
->orderBy('fd.object_id', 'ASC')
->addOrderBy('fd.updated_at', 'ASC')
->setParameter('type', Field::OBJECT_OPPORTUNITY)
->setParameter('crm', $crmId)
;
if (! empty($crmFields)) {
$fields = array_map(fn ($value): string => '"' . $value . '"', $crmFields);
$qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');
}
return $qb->executeQuery()->fetchAllAssociative();
}
public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAssociative();
}
public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('COALESCE(opp.currency_code, "' . $defaultCurrency . '") AS currency')
->addSelect('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
->groupBy('currency')
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAllAssociative();
}
public function getDealActivities(CriteriaInterface $criteria): array
{
$qb = Activity::with(['participants', 'user'])
->where('opportunity_id', $criteria->getOpportunityId())
->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())
->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())
->orderBy($criteria->getSortBy(), $criteria->getSortDirection())
;
// Should we filter activities by criteria? It's intended to filter deals.
return $qb->get()->all();
}
public function getStages(CriteriaInterface $criteria): array
{
$qb = new QueryBuilder($this->connection);
$qb
->select('id', 'label', 'sequence')
->from('stages', 's')
->where('crm_configuration_id = :crm_configuration_id')
->andWhere('type = :type')
->orderBy('sequence', 'ASC')
->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())
->setParameter('type', Stage::TYPE_OPPORTUNITY);
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$result[$row['id']] = [
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
public function getConfigurationStages(Configuration $configuration): Collection
{
return $configuration
->stages()
->where('type', Stage::TYPE_OPPORTUNITY)
->get();
}
public function getPipelineData(Configuration $crm): array
{
$qb = new QueryBuilder($this->connection);
$provider = $crm->provider;
$qb
->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')
->from('stages', 's')
->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')
->where('s.crm_configuration_id = :crm_configuration_id')
->andWhere('s.type = :type')
->orderBy('bps.business_process_id', 'ASC')
->addOrderBy('s.sequence', 'ASC')
->setParameter('crm_configuration_id', $crm->id)
->setParameter('type', Stage::TYPE_OPPORTUNITY)
;
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];
$result[$row['pipeline_id']][] = [
'value' => $value,
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
private function createQueryBuilder(string $realm): QueryBuilder
{
return (new QueryBuilder($this->connection))
->setRealm($realm)
->from('opportunities', 'opp')
->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')
->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')
->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')
;
}
/**
* Applies all applicable visitors and returns the IDs of the executed ones
*
* @return string[]
*/
private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array
{
$queryVisitors = [];
foreach ($this->visitors as $visitor) {
if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {
$visitor->visit($queryBuilder, $criteria);
$queryVisitors[] = $visitor->getIdentifier();
}
}
return $queryVisitors;
}
private function hydrateStages(array $deals): array
{
foreach ($this->fetchStages(array_keys($deals)) as $stage) {
$oppId = (int) $stage['opportunity_id'];
if (! isset($deals[$oppId])) {
continue; // or throw??!
}
$deals[$oppId]['stages'][] = [
'id' => $stage['stage_id'],
'name' => $stage['label'],
'enteredAt' => $stage['created_at'],
];
}
return $deals;
}
/**
* @param int[] $dealIds
*/
private function fetchStages(array $dealIds): array
{
if (empty($dealIds)) {
return [];
}
$qb = new QueryBuilder($this->connection);
$qb
->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')
->from('opportunity_stages', 'os')
->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')
->where($qb->expr()->in('os.opportunity_id', $dealIds))
->orderBy('os.opportunity_id', 'ASC')
->addOrderBy('s.created_at', 'ASC')
;
return $qb->executeQuery()->fetchAllAssociative();
}
private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array
{
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$data = [
'uuid' => RequiresUUID::toNormal($row['uuid']),
'name' => $row['name'],
'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),
'account' => [
'name' => $row['acc_name'],
'url' => $crmService->generateProviderUrl(
providerId: $row['acc_provider_id'],
objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'
),
],
'owner' => null,
'rawValue' => [
'amount' => (float) $row['value'],
'currency' => $row['currency_code'],
],
'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),
'openDate' => $row['remotely_created_at'] ?? null,
'closeDate' => $row['close_date'] ?? null,
'stages' => [],
'currentPipelineId' => $row['pipeline_id'],
'currentStage' => [
'id' => $row['stage_id'],
'enteredAt' => $row['stage_updated_at'],
],
'currentStageUpdatedAt' => $row['stage_updated_at'],
'isClosed' => (bool) $row['is_closed'],
'isWon' => (bool) $row['is_won'],
];
if (isset($row['owner_uuid'])) {
$data['owner'] = [
'uuid' => RequiresUUID::toNormal($row['owner_uuid']),
'name' => $row['owner_name'],
'photoUrl' => $row['owner_photo'] === null
? null
: client_cdn($row['owner_photo'], $team),
'id' => $row['owner_id'],
'job' => $row['owner_job'],
];
}
$result[(int) $row['opp_id']] = $data;
}
return $this->hydrateStages($result);
}
private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder
{
$qb = clone $queryBuilder;
$qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');
$qb
->select(...[
'opp.id as opp_id',
'opp.uuid',
'opp.name',
'opp.value',
'opp.currency_code',
'opp.close_date',
'opp.remotely_created_at',
'opp.is_closed',
'opp.is_won',
])
->addSelect(...[
'usr.uuid as owner_uuid',
'usr.name AS owner_name',
'usr.photo_path as owner_photo',
'usr.id AS owner_id',
'jt.name as owner_job',
])
->addSelect('opp.stage_id', 'opp.stage_updated_at')
->addSelect(...[
'acc.name AS acc_name',
'acc.is_internal as acc_is_internal',
'opp.stage_updated_at',
'acc.crm_provider_id AS acc_provider_id',
'opp.crm_provider_id AS opp_provider_id',
])
->addSelect('rt.business_process_id AS pipeline_id')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'));
return $qb;
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws SocialAccountTokenInvalidException
*/
private function getCrmService(Team $team): ServiceInterface
{
$crmService = $this->providerRegistry->get($team->crm->provider);
$crmService->setConfiguration($team->crm);
if ($crmService instanceof UrlGeneratorInterface) {
$crmService->setCrmUrlGenerator($team->crm);
}
return $crmService;
}
/**
*
* @return Generator<DealData>
*/
public function getForecastData(DealsFilter $filter): Generator
{
$opportunities = DB::query()
->select([
'o.value',
'o.close_date',
'o.currency_code',
'o.is_won',
'o.is_closed',
'o.probability',
'o.forecast_category',
])
->from('opportunities', 'o')
->join('users', 'users.id', '=', 'o.user_id')
->join('groups', 'groups.id', '=', 'users.group_id')
->where('users.team_id', $filter->getTeam()->getId())
->where('o.close_date', '>=', $filter->getStartDate())
->where('o.close_date', '<=', $filter->getEndDate())
->where('o.currency_code', $filter->getCurrency())
->where('o.deleted_at', '=', null)
;
$userUuidList = $filter->getUserUuidList();
if (! empty($userUuidList)) {
$userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);
$opportunities->whereIn('users.uuid', $userUuidList);
}
$groupUuidList = $filter->getGroupUuidList();
if (! empty($groupUuidList)) {
$groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);
$opportunities->whereIn('groups.uuid', $groupUuidList);
}
foreach ($opportunities->cursor() as $row) {
yield new DealData(
(float) $row->value,
$row->close_date,
! empty($row->is_won),
! empty($row->is_closed),
$row->probability ?: 0,
$row->forecast_category ?: '',
);
}
}
public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection
{
return $user->subscriptionSets()
->where(static function (Eloquent\Builder $query): void {
$query
->whereNull('expired_at')
->orWhere('expired_at', '>=', now());
})
->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {
$join
->on('subscription_set_id', '=', 'activity_subscription_sets.id');
$join
->where('followable_type', Models\Activity\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)
->whereIn('followable_id', $opportunityIds);
})
->pluck('followable_id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
72984
|
|
72938
|
NULL
|
0
|
2026-04-23T06:24:34.502611+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776925474502_m2.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsRepository.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
16
7
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active Ask Jiminny reports whose expiry date has passed.
*
* @return Collection<AutomatedReport>
*/
public function getExpiredActiveAskJiminnyReports(): Collection
{
return AutomatedReport::where('status', true)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereNotNull('expires_at')
->where('expires_at', '<', now()->toDateString())
->get();
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('report_id', $report->getId())
->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])
->latest()
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Restrict a query on the automated_reports table to reports the given user is allowed to see.
*
* Matches the customer-facing audience:
* - explicit user recipients (recipients.users)
* - members of any of the report's groups (Ask Jiminny reports)
*/
private function applyUserAccessScope(Builder $query, User $user): void
{
$userId = $user->getId();
$groupId = $user->getGroupId();
$query
->where('automated_reports.team_id', $user->getTeamId())
->where(function (Builder $q) use ($userId, $groupId): void {
$q->whereJsonContains('automated_reports.recipients->users', $userId);
if ($groupId !== null) {
$q->orWhere(function (Builder $sub) use ($groupId): void {
$sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereJsonContains('automated_reports.groups', $groupId);
});
}
});
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
36
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\DealInsights;
use Doctrine\DBAL\Connection;
use Generator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Jiminny\Component\DealInsights\Forecast\DealData;
use Jiminny\Component\DealInsights\Forecast\DealsFilter;
use Jiminny\Component\DealInsights\QueryBuilder\QueryBuilder;
use Jiminny\Component\DealInsights\QueryBuilder\Visitor\QueryBuilderVisitorInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Services\Crm\IntegrationApp\DTO\Utils\UrlGeneratorInterface;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class DealsRepository implements DealsRepositoryInterface
{
private Connection $connection;
private ProviderRegistry $providerRegistry;
/**
* @var QueryBuilderVisitorInterface[]
*/
private array $visitors = [];
/**
* @param QueryBuilderVisitorInterface[] $visitors
*/
public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])
{
$this->connection = $connection;
$this->providerRegistry = $crmProviderRegistry;
foreach ($visitors as $visitor) {
$this->visitors[$visitor->getIdentifier()] = $visitor;
}
}
public function getDeals(CriteriaInterface $criteria): array
{
$context = $criteria->getContext();
$team = $context->getTeam();
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$this->visit($qb, $criteria);
return $this->execute($team, $crmService, $qb);
}
public function getDeal(Team $team, int $id): array
{
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$qb->andWhere('opp.id = :id')->setParameter('id', $id);
return $this->execute($team, $crmService, $qb);
}
public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])
{
$qb = new QueryBuilder($this->connection);
$qb
->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')
->from('crm_fields', 'f')
->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')
->where('f.crm_configuration_id = :crm')
->andWhere('f.object_type = :type')
->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')
->orderBy('fd.object_id', 'ASC')
->addOrderBy('fd.updated_at', 'ASC')
->setParameter('type', Field::OBJECT_OPPORTUNITY)
->setParameter('crm', $crmId)
;
if (! empty($crmFields)) {
$fields = array_map(fn ($value): string => '"' . $value . '"', $crmFields);
$qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');
}
return $qb->executeQuery()->fetchAllAssociative();
}
public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAssociative();
}
public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('COALESCE(opp.currency_code, "' . $defaultCurrency . '") AS currency')
->addSelect('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
->groupBy('currency')
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAllAssociative();
}
public function getDealActivities(CriteriaInterface $criteria): array
{
$qb = Activity::with(['participants', 'user'])
->where('opportunity_id', $criteria->getOpportunityId())
->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())
->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())
->orderBy($criteria->getSortBy(), $criteria->getSortDirection())
;
// Should we filter activities by criteria? It's intended to filter deals.
return $qb->get()->all();
}
public function getStages(CriteriaInterface $criteria): array
{
$qb = new QueryBuilder($this->connection);
$qb
->select('id', 'label', 'sequence')
->from('stages', 's')
->where('crm_configuration_id = :crm_configuration_id')
->andWhere('type = :type')
->orderBy('sequence', 'ASC')
->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())
->setParameter('type', Stage::TYPE_OPPORTUNITY);
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$result[$row['id']] = [
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
public function getConfigurationStages(Configuration $configuration): Collection
{
return $configuration
->stages()
->where('type', Stage::TYPE_OPPORTUNITY)
->get();
}
public function getPipelineData(Configuration $crm): array
{
$qb = new QueryBuilder($this->connection);
$provider = $crm->provider;
$qb
->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')
->from('stages', 's')
->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')
->where('s.crm_configuration_id = :crm_configuration_id')
->andWhere('s.type = :type')
->orderBy('bps.business_process_id', 'ASC')
->addOrderBy('s.sequence', 'ASC')
->setParameter('crm_configuration_id', $crm->id)
->setParameter('type', Stage::TYPE_OPPORTUNITY)
;
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];
$result[$row['pipeline_id']][] = [
'value' => $value,
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
private function createQueryBuilder(string $realm): QueryBuilder
{
return (new QueryBuilder($this->connection))
->setRealm($realm)
->from('opportunities', 'opp')
->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')
->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')
->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')
;
}
/**
* Applies all applicable visitors and returns the IDs of the executed ones
*
* @return string[]
*/
private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array
{
$queryVisitors = [];
foreach ($this->visitors as $visitor) {
if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {
$visitor->visit($queryBuilder, $criteria);
$queryVisitors[] = $visitor->getIdentifier();
}
}
return $queryVisitors;
}
private function hydrateStages(array $deals): array
{
foreach ($this->fetchStages(array_keys($deals)) as $stage) {
$oppId = (int) $stage['opportunity_id'];
if (! isset($deals[$oppId])) {
continue; // or throw??!
}
$deals[$oppId]['stages'][] = [
'id' => $stage['stage_id'],
'name' => $stage['label'],
'enteredAt' => $stage['created_at'],
];
}
return $deals;
}
/**
* @param int[] $dealIds
*/
private function fetchStages(array $dealIds): array
{
if (empty($dealIds)) {
return [];
}
$qb = new QueryBuilder($this->connection);
$qb
->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')
->from('opportunity_stages', 'os')
->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')
->where($qb->expr()->in('os.opportunity_id', $dealIds))
->orderBy('os.opportunity_id', 'ASC')
->addOrderBy('s.created_at', 'ASC')
;
return $qb->executeQuery()->fetchAllAssociative();
}
private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array
{
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$data = [
'uuid' => RequiresUUID::toNormal($row['uuid']),
'name' => $row['name'],
'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),
'account' => [
'name' => $row['acc_name'],
'url' => $crmService->generateProviderUrl(
providerId: $row['acc_provider_id'],
objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'
),
],
'owner' => null,
'rawValue' => [
'amount' => (float) $row['value'],
'currency' => $row['currency_code'],
],
'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),
'openDate' => $row['remotely_created_at'] ?? null,
'closeDate' => $row['close_date'] ?? null,
'stages' => [],
'currentPipelineId' => $row['pipeline_id'],
'currentStage' => [
'id' => $row['stage_id'],
'enteredAt' => $row['stage_updated_at'],
],
'currentStageUpdatedAt' => $row['stage_updated_at'],
'isClosed' => (bool) $row['is_closed'],
'isWon' => (bool) $row['is_won'],
];
if (isset($row['owner_uuid'])) {
$data['owner'] = [
'uuid' => RequiresUUID::toNormal($row['owner_uuid']),
'name' => $row['owner_name'],
'photoUrl' => $row['owner_photo'] === null
? null
: client_cdn($row['owner_photo'], $team),
'id' => $row['owner_id'],
'job' => $row['owner_job'],
];
}
$result[(int) $row['opp_id']] = $data;
}
return $this->hydrateStages($result);
}
private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder
{
$qb = clone $queryBuilder;
$qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');
$qb
->select(...[
'opp.id as opp_id',
'opp.uuid',
'opp.name',
'opp.value',
'opp.currency_code',
'opp.close_date',
'opp.remotely_created_at',
'opp.is_closed',
'opp.is_won',
])
->addSelect(...[
'usr.uuid as owner_uuid',
'usr.name AS owner_name',
'usr.photo_path as owner_photo',
'usr.id AS owner_id',
'jt.name as owner_job',
])
->addSelect('opp.stage_id', 'opp.stage_updated_at')
->addSelect(...[
'acc.name AS acc_name',
'acc.is_internal as acc_is_internal',
'opp.stage_updated_at',
'acc.crm_provider_id AS acc_provider_id',
'opp.crm_provider_id AS opp_provider_id',
])
->addSelect('rt.business_process_id AS pipeline_id')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'));
return $qb;
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws SocialAccountTokenInvalidException
*/
private function getCrmService(Team $team): ServiceInterface
{
$crmService = $this->providerRegistry->get($team->crm->provider);
$crmService->setConfiguration($team->crm);
if ($crmService instanceof UrlGeneratorInterface) {
$crmService->setCrmUrlGenerator($team->crm);
}
return $crmService;
}
/**
*
* @return Generator<DealData>
*/
public function getForecastData(DealsFilter $filter): Generator
{
$opportunities = DB::query()
->select([
'o.value',
'o.close_date',
'o.currency_code',
'o.is_won',
'o.is_closed',
'o.probability',
'o.forecast_category',
])
->from('opportunities', 'o')
->join('users', 'users.id', '=', 'o.user_id')
->join('groups', 'groups.id', '=', 'users.group_id')
->where('users.team_id', $filter->getTeam()->getId())
->where('o.close_date', '>=', $filter->getStartDate())
->where('o.close_date', '<=', $filter->getEndDate())
->where('o.currency_code', $filter->getCurrency())
->where('o.deleted_at', '=', null)
;
$userUuidList = $filter->getUserUuidList();
if (! empty($userUuidList)) {
$userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);
$opportunities->whereIn('users.uuid', $userUuidList);
}
$groupUuidList = $filter->getGroupUuidList();
if (! empty($groupUuidList)) {
$groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);
$opportunities->whereIn('groups.uuid', $groupUuidList);
}
foreach ($opportunities->cursor() as $row) {
yield new DealData(
(float) $row->value,
$row->close_date,
! empty($row->is_won),
! empty($row->is_closed),
$row->probability ?: 0,
$row->forecast_category ?: '',
);
}
}
public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection
{
return $user->subscriptionSets()
->where(static function (Eloquent\Builder $query): void {
$query
->whereNull('expired_at')
->orWhere('expired_at', '>=', now());
})
->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {
$join
->on('subscription_set_id', '=', 'activity_subscription_sets.id');
$join
->where('followable_type', Models\Activity\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)
->whereIn('followable_id', $opportunityIds);
})
->pluck('followable_id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.25731382,"top":0.019952115,"width":0.03856383,"height":0.025538707},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20157-AJ-report-not-send-notification, menu","depth":5,"bounds":{"left":0.29587767,"top":0.019952115,"width":0.10139628,"height":0.025538707},"help_text":"Git Branch: JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.7972075,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"RequestGenerateAskJiminnyReportJobTest","depth":6,"bounds":{"left":0.8125,"top":0.019952115,"width":0.10305851,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'RequestGenerateAskJiminnyReportJobTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"16","depth":4,"bounds":{"left":0.6030585,"top":0.17478053,"width":0.009640957,"height":0.015163607},"role_description":"text"},{"role":"AXStaticText","text":"7","depth":4,"bounds":{"left":0.6146942,"top":0.17478053,"width":0.0076462766,"height":0.015163607},"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.6243351,"top":0.17478053,"width":0.00731383,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.63331115,"top":0.17318435,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.640625,"top":0.17318435,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active Ask Jiminny reports whose expiry date has passed.\n *\n * @return Collection<AutomatedReport>\n */\n public function getExpiredActiveAskJiminnyReports(): Collection\n {\n return AutomatedReport::where('status', true)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereNotNull('expires_at')\n ->where('expires_at', '<', now()->toDateString())\n ->get();\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('report_id', $report->getId())\n ->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])\n ->latest()\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Restrict a query on the automated_reports table to reports the given user is allowed to see.\n *\n * Matches the customer-facing audience:\n * - explicit user recipients (recipients.users)\n * - members of any of the report's groups (Ask Jiminny reports)\n */\n private function applyUserAccessScope(Builder $query, User $user): void\n {\n $userId = $user->getId();\n $groupId = $user->getGroupId();\n\n $query\n ->where('automated_reports.team_id', $user->getTeamId())\n ->where(function (Builder $q) use ($userId, $groupId): void {\n $q->whereJsonContains('automated_reports.recipients->users', $userId);\n\n if ($groupId !== null) {\n $q->orWhere(function (Builder $sub) use ($groupId): void {\n $sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereJsonContains('automated_reports.groups', $groupId);\n });\n }\n });\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active Ask Jiminny reports whose expiry date has passed.\n *\n * @return Collection<AutomatedReport>\n */\n public function getExpiredActiveAskJiminnyReports(): Collection\n {\n return AutomatedReport::where('status', true)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereNotNull('expires_at')\n ->where('expires_at', '<', now()->toDateString())\n ->get();\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('report_id', $report->getId())\n ->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])\n ->latest()\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Restrict a query on the automated_reports table to reports the given user is allowed to see.\n *\n * Matches the customer-facing audience:\n * - explicit user recipients (recipients.users)\n * - members of any of the report's groups (Ask Jiminny reports)\n */\n private function applyUserAccessScope(Builder $query, User $user): void\n {\n $userId = $user->getId();\n $groupId = $user->getGroupId();\n\n $query\n ->where('automated_reports.team_id', $user->getTeamId())\n ->where(function (Builder $q) use ($userId, $groupId): void {\n $q->whereJsonContains('automated_reports.recipients->users', $userId);\n\n if ($groupId !== null) {\n $q->orWhere(function (Builder $sub) use ($groupId): void {\n $sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->whereJsonContains('automated_reports.groups', $groupId);\n });\n }\n });\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"36","depth":4,"bounds":{"left":0.69980055,"top":0.2490024,"width":0.010305851,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7117686,"top":0.24740623,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7190825,"top":0.24740623,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\DealInsights;\n\nuse Doctrine\\DBAL\\Connection;\nuse Generator;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealData;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealsFilter;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\QueryBuilder;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\Visitor\\QueryBuilderVisitorInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Services\\Crm\\IntegrationApp\\DTO\\Utils\\UrlGeneratorInterface;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Eloquent;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass DealsRepository implements DealsRepositoryInterface\n{\n private Connection $connection;\n\n private ProviderRegistry $providerRegistry;\n\n /**\n * @var QueryBuilderVisitorInterface[]\n */\n private array $visitors = [];\n\n /**\n * @param QueryBuilderVisitorInterface[] $visitors\n */\n public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])\n {\n $this->connection = $connection;\n $this->providerRegistry = $crmProviderRegistry;\n\n foreach ($visitors as $visitor) {\n $this->visitors[$visitor->getIdentifier()] = $visitor;\n }\n }\n\n public function getDeals(CriteriaInterface $criteria): array\n {\n $context = $criteria->getContext();\n $team = $context->getTeam();\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n\n $this->visit($qb, $criteria);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getDeal(Team $team, int $id): array\n {\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n $qb->andWhere('opp.id = :id')->setParameter('id', $id);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')\n ->from('crm_fields', 'f')\n ->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')\n ->where('f.crm_configuration_id = :crm')\n ->andWhere('f.object_type = :type')\n ->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')\n ->orderBy('fd.object_id', 'ASC')\n ->addOrderBy('fd.updated_at', 'ASC')\n\n ->setParameter('type', Field::OBJECT_OPPORTUNITY)\n ->setParameter('crm', $crmId)\n ;\n\n if (! empty($crmFields)) {\n $fields = array_map(fn ($value): string => '\"' . $value . '\"', $crmFields);\n $qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');\n }\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAssociative();\n }\n\n public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('COALESCE(opp.currency_code, \"' . $defaultCurrency . '\") AS currency')\n ->addSelect('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ->groupBy('currency')\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getDealActivities(CriteriaInterface $criteria): array\n {\n $qb = Activity::with(['participants', 'user'])\n ->where('opportunity_id', $criteria->getOpportunityId())\n ->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())\n ->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())\n ->orderBy($criteria->getSortBy(), $criteria->getSortDirection())\n ;\n\n // Should we filter activities by criteria? It's intended to filter deals.\n\n return $qb->get()->all();\n }\n\n public function getStages(CriteriaInterface $criteria): array\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('id', 'label', 'sequence')\n ->from('stages', 's')\n ->where('crm_configuration_id = :crm_configuration_id')\n ->andWhere('type = :type')\n ->orderBy('sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())\n ->setParameter('type', Stage::TYPE_OPPORTUNITY);\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $result[$row['id']] = [\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n public function getConfigurationStages(Configuration $configuration): Collection\n {\n return $configuration\n ->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get();\n }\n\n public function getPipelineData(Configuration $crm): array\n {\n $qb = new QueryBuilder($this->connection);\n $provider = $crm->provider;\n\n $qb\n ->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')\n ->from('stages', 's')\n ->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')\n ->where('s.crm_configuration_id = :crm_configuration_id')\n ->andWhere('s.type = :type')\n ->orderBy('bps.business_process_id', 'ASC')\n ->addOrderBy('s.sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $crm->id)\n ->setParameter('type', Stage::TYPE_OPPORTUNITY)\n ;\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];\n $result[$row['pipeline_id']][] = [\n 'value' => $value,\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n private function createQueryBuilder(string $realm): QueryBuilder\n {\n return (new QueryBuilder($this->connection))\n ->setRealm($realm)\n ->from('opportunities', 'opp')\n ->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')\n ->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')\n ->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')\n ;\n }\n\n /**\n * Applies all applicable visitors and returns the IDs of the executed ones\n *\n * @return string[]\n */\n private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array\n {\n $queryVisitors = [];\n\n foreach ($this->visitors as $visitor) {\n if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {\n $visitor->visit($queryBuilder, $criteria);\n\n $queryVisitors[] = $visitor->getIdentifier();\n }\n }\n\n return $queryVisitors;\n }\n\n private function hydrateStages(array $deals): array\n {\n foreach ($this->fetchStages(array_keys($deals)) as $stage) {\n $oppId = (int) $stage['opportunity_id'];\n\n if (! isset($deals[$oppId])) {\n continue; // or throw??!\n }\n\n $deals[$oppId]['stages'][] = [\n 'id' => $stage['stage_id'],\n 'name' => $stage['label'],\n 'enteredAt' => $stage['created_at'],\n ];\n }\n\n return $deals;\n }\n\n /**\n * @param int[] $dealIds\n */\n private function fetchStages(array $dealIds): array\n {\n if (empty($dealIds)) {\n return [];\n }\n\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')\n ->from('opportunity_stages', 'os')\n ->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')\n ->where($qb->expr()->in('os.opportunity_id', $dealIds))\n ->orderBy('os.opportunity_id', 'ASC')\n ->addOrderBy('s.created_at', 'ASC')\n ;\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array\n {\n $result = [];\n\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $data = [\n 'uuid' => RequiresUUID::toNormal($row['uuid']),\n 'name' => $row['name'],\n 'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),\n 'account' => [\n 'name' => $row['acc_name'],\n 'url' => $crmService->generateProviderUrl(\n providerId: $row['acc_provider_id'],\n objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'\n ),\n ],\n 'owner' => null,\n 'rawValue' => [\n 'amount' => (float) $row['value'],\n 'currency' => $row['currency_code'],\n ],\n 'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),\n 'openDate' => $row['remotely_created_at'] ?? null,\n 'closeDate' => $row['close_date'] ?? null,\n 'stages' => [],\n 'currentPipelineId' => $row['pipeline_id'],\n 'currentStage' => [\n 'id' => $row['stage_id'],\n 'enteredAt' => $row['stage_updated_at'],\n ],\n 'currentStageUpdatedAt' => $row['stage_updated_at'],\n 'isClosed' => (bool) $row['is_closed'],\n 'isWon' => (bool) $row['is_won'],\n ];\n\n if (isset($row['owner_uuid'])) {\n $data['owner'] = [\n 'uuid' => RequiresUUID::toNormal($row['owner_uuid']),\n 'name' => $row['owner_name'],\n 'photoUrl' => $row['owner_photo'] === null\n ? null\n : client_cdn($row['owner_photo'], $team),\n 'id' => $row['owner_id'],\n 'job' => $row['owner_job'],\n ];\n }\n\n $result[(int) $row['opp_id']] = $data;\n }\n\n return $this->hydrateStages($result);\n }\n\n private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder\n {\n $qb = clone $queryBuilder;\n $qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');\n\n $qb\n ->select(...[\n 'opp.id as opp_id',\n 'opp.uuid',\n 'opp.name',\n 'opp.value',\n 'opp.currency_code',\n 'opp.close_date',\n 'opp.remotely_created_at',\n 'opp.is_closed',\n 'opp.is_won',\n ])\n ->addSelect(...[\n 'usr.uuid as owner_uuid',\n 'usr.name AS owner_name',\n 'usr.photo_path as owner_photo',\n 'usr.id AS owner_id',\n 'jt.name as owner_job',\n ])\n ->addSelect('opp.stage_id', 'opp.stage_updated_at')\n ->addSelect(...[\n 'acc.name AS acc_name',\n 'acc.is_internal as acc_is_internal',\n 'opp.stage_updated_at',\n 'acc.crm_provider_id AS acc_provider_id',\n 'opp.crm_provider_id AS opp_provider_id',\n ])\n ->addSelect('rt.business_process_id AS pipeline_id')\n\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'));\n\n return $qb;\n }\n\n /**\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws SocialAccountTokenInvalidException\n */\n private function getCrmService(Team $team): ServiceInterface\n {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n $crmService->setConfiguration($team->crm);\n if ($crmService instanceof UrlGeneratorInterface) {\n $crmService->setCrmUrlGenerator($team->crm);\n }\n\n return $crmService;\n }\n\n /**\n *\n * @return Generator<DealData>\n */\n public function getForecastData(DealsFilter $filter): Generator\n {\n $opportunities = DB::query()\n ->select([\n 'o.value',\n 'o.close_date',\n 'o.currency_code',\n 'o.is_won',\n 'o.is_closed',\n 'o.probability',\n 'o.forecast_category',\n ])\n ->from('opportunities', 'o')\n ->join('users', 'users.id', '=', 'o.user_id')\n ->join('groups', 'groups.id', '=', 'users.group_id')\n ->where('users.team_id', $filter->getTeam()->getId())\n ->where('o.close_date', '>=', $filter->getStartDate())\n ->where('o.close_date', '<=', $filter->getEndDate())\n ->where('o.currency_code', $filter->getCurrency())\n ->where('o.deleted_at', '=', null)\n ;\n\n $userUuidList = $filter->getUserUuidList();\n if (! empty($userUuidList)) {\n $userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);\n\n $opportunities->whereIn('users.uuid', $userUuidList);\n }\n\n $groupUuidList = $filter->getGroupUuidList();\n if (! empty($groupUuidList)) {\n $groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);\n\n $opportunities->whereIn('groups.uuid', $groupUuidList);\n }\n\n foreach ($opportunities->cursor() as $row) {\n yield new DealData(\n (float) $row->value,\n $row->close_date,\n ! empty($row->is_won),\n ! empty($row->is_closed),\n $row->probability ?: 0,\n $row->forecast_category ?: '',\n );\n }\n }\n\n public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection\n {\n return $user->subscriptionSets()\n ->where(static function (Eloquent\\Builder $query): void {\n $query\n ->whereNull('expired_at')\n ->orWhere('expired_at', '>=', now());\n })\n ->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n $join\n ->where('followable_type', Models\\Activity\\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)\n ->whereIn('followable_id', $opportunityIds);\n })\n ->pluck('followable_id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\DealInsights;\n\nuse Doctrine\\DBAL\\Connection;\nuse Generator;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealData;\nuse Jiminny\\Component\\DealInsights\\Forecast\\DealsFilter;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\QueryBuilder;\nuse Jiminny\\Component\\DealInsights\\QueryBuilder\\Visitor\\QueryBuilderVisitorInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Services\\Crm\\IntegrationApp\\DTO\\Utils\\UrlGeneratorInterface;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Eloquent;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass DealsRepository implements DealsRepositoryInterface\n{\n private Connection $connection;\n\n private ProviderRegistry $providerRegistry;\n\n /**\n * @var QueryBuilderVisitorInterface[]\n */\n private array $visitors = [];\n\n /**\n * @param QueryBuilderVisitorInterface[] $visitors\n */\n public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])\n {\n $this->connection = $connection;\n $this->providerRegistry = $crmProviderRegistry;\n\n foreach ($visitors as $visitor) {\n $this->visitors[$visitor->getIdentifier()] = $visitor;\n }\n }\n\n public function getDeals(CriteriaInterface $criteria): array\n {\n $context = $criteria->getContext();\n $team = $context->getTeam();\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n\n $this->visit($qb, $criteria);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getDeal(Team $team, int $id): array\n {\n $crmService = $this->getCrmService($team);\n\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);\n $qb = $this->getSearchSelectAndWhereClauses($qb);\n $qb->andWhere('opp.id = :id')->setParameter('id', $id);\n\n return $this->execute($team, $crmService, $qb);\n }\n\n public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')\n ->from('crm_fields', 'f')\n ->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')\n ->where('f.crm_configuration_id = :crm')\n ->andWhere('f.object_type = :type')\n ->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')\n ->orderBy('fd.object_id', 'ASC')\n ->addOrderBy('fd.updated_at', 'ASC')\n\n ->setParameter('type', Field::OBJECT_OPPORTUNITY)\n ->setParameter('crm', $crmId)\n ;\n\n if (! empty($crmFields)) {\n $fields = array_map(fn ($value): string => '\"' . $value . '\"', $crmFields);\n $qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');\n }\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAssociative();\n }\n\n public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array\n {\n $qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);\n\n $qb\n ->select('COALESCE(opp.currency_code, \"' . $defaultCurrency . '\") AS currency')\n ->addSelect('SUM(opp.value) as total')\n ->addSelect('count(*) as `count`')\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'))\n ->groupBy('currency')\n ;\n\n $this->visit($qb, $criteria);\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n public function getDealActivities(CriteriaInterface $criteria): array\n {\n $qb = Activity::with(['participants', 'user'])\n ->where('opportunity_id', $criteria->getOpportunityId())\n ->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())\n ->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())\n ->orderBy($criteria->getSortBy(), $criteria->getSortDirection())\n ;\n\n // Should we filter activities by criteria? It's intended to filter deals.\n\n return $qb->get()->all();\n }\n\n public function getStages(CriteriaInterface $criteria): array\n {\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('id', 'label', 'sequence')\n ->from('stages', 's')\n ->where('crm_configuration_id = :crm_configuration_id')\n ->andWhere('type = :type')\n ->orderBy('sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())\n ->setParameter('type', Stage::TYPE_OPPORTUNITY);\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $result[$row['id']] = [\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n public function getConfigurationStages(Configuration $configuration): Collection\n {\n return $configuration\n ->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get();\n }\n\n public function getPipelineData(Configuration $crm): array\n {\n $qb = new QueryBuilder($this->connection);\n $provider = $crm->provider;\n\n $qb\n ->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')\n ->from('stages', 's')\n ->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')\n ->where('s.crm_configuration_id = :crm_configuration_id')\n ->andWhere('s.type = :type')\n ->orderBy('bps.business_process_id', 'ASC')\n ->addOrderBy('s.sequence', 'ASC')\n\n ->setParameter('crm_configuration_id', $crm->id)\n ->setParameter('type', Stage::TYPE_OPPORTUNITY)\n ;\n\n $result = [];\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];\n $result[$row['pipeline_id']][] = [\n 'value' => $value,\n 'label' => $row['label'],\n 'sequence' => $row['sequence'],\n ];\n }\n\n return $result;\n }\n\n private function createQueryBuilder(string $realm): QueryBuilder\n {\n return (new QueryBuilder($this->connection))\n ->setRealm($realm)\n ->from('opportunities', 'opp')\n ->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')\n ->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')\n ->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')\n ;\n }\n\n /**\n * Applies all applicable visitors and returns the IDs of the executed ones\n *\n * @return string[]\n */\n private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array\n {\n $queryVisitors = [];\n\n foreach ($this->visitors as $visitor) {\n if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {\n $visitor->visit($queryBuilder, $criteria);\n\n $queryVisitors[] = $visitor->getIdentifier();\n }\n }\n\n return $queryVisitors;\n }\n\n private function hydrateStages(array $deals): array\n {\n foreach ($this->fetchStages(array_keys($deals)) as $stage) {\n $oppId = (int) $stage['opportunity_id'];\n\n if (! isset($deals[$oppId])) {\n continue; // or throw??!\n }\n\n $deals[$oppId]['stages'][] = [\n 'id' => $stage['stage_id'],\n 'name' => $stage['label'],\n 'enteredAt' => $stage['created_at'],\n ];\n }\n\n return $deals;\n }\n\n /**\n * @param int[] $dealIds\n */\n private function fetchStages(array $dealIds): array\n {\n if (empty($dealIds)) {\n return [];\n }\n\n $qb = new QueryBuilder($this->connection);\n\n $qb\n ->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')\n ->from('opportunity_stages', 'os')\n ->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')\n ->where($qb->expr()->in('os.opportunity_id', $dealIds))\n ->orderBy('os.opportunity_id', 'ASC')\n ->addOrderBy('s.created_at', 'ASC')\n ;\n\n return $qb->executeQuery()->fetchAllAssociative();\n }\n\n private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array\n {\n $result = [];\n\n foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {\n $data = [\n 'uuid' => RequiresUUID::toNormal($row['uuid']),\n 'name' => $row['name'],\n 'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),\n 'account' => [\n 'name' => $row['acc_name'],\n 'url' => $crmService->generateProviderUrl(\n providerId: $row['acc_provider_id'],\n objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'\n ),\n ],\n 'owner' => null,\n 'rawValue' => [\n 'amount' => (float) $row['value'],\n 'currency' => $row['currency_code'],\n ],\n 'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),\n 'openDate' => $row['remotely_created_at'] ?? null,\n 'closeDate' => $row['close_date'] ?? null,\n 'stages' => [],\n 'currentPipelineId' => $row['pipeline_id'],\n 'currentStage' => [\n 'id' => $row['stage_id'],\n 'enteredAt' => $row['stage_updated_at'],\n ],\n 'currentStageUpdatedAt' => $row['stage_updated_at'],\n 'isClosed' => (bool) $row['is_closed'],\n 'isWon' => (bool) $row['is_won'],\n ];\n\n if (isset($row['owner_uuid'])) {\n $data['owner'] = [\n 'uuid' => RequiresUUID::toNormal($row['owner_uuid']),\n 'name' => $row['owner_name'],\n 'photoUrl' => $row['owner_photo'] === null\n ? null\n : client_cdn($row['owner_photo'], $team),\n 'id' => $row['owner_id'],\n 'job' => $row['owner_job'],\n ];\n }\n\n $result[(int) $row['opp_id']] = $data;\n }\n\n return $this->hydrateStages($result);\n }\n\n private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder\n {\n $qb = clone $queryBuilder;\n $qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');\n\n $qb\n ->select(...[\n 'opp.id as opp_id',\n 'opp.uuid',\n 'opp.name',\n 'opp.value',\n 'opp.currency_code',\n 'opp.close_date',\n 'opp.remotely_created_at',\n 'opp.is_closed',\n 'opp.is_won',\n ])\n ->addSelect(...[\n 'usr.uuid as owner_uuid',\n 'usr.name AS owner_name',\n 'usr.photo_path as owner_photo',\n 'usr.id AS owner_id',\n 'jt.name as owner_job',\n ])\n ->addSelect('opp.stage_id', 'opp.stage_updated_at')\n ->addSelect(...[\n 'acc.name AS acc_name',\n 'acc.is_internal as acc_is_internal',\n 'opp.stage_updated_at',\n 'acc.crm_provider_id AS acc_provider_id',\n 'opp.crm_provider_id AS opp_provider_id',\n ])\n ->addSelect('rt.business_process_id AS pipeline_id')\n\n ->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users\n ->andWhere($qb->expr()->isNull('opp.deleted_at'));\n\n return $qb;\n }\n\n /**\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws SocialAccountTokenInvalidException\n */\n private function getCrmService(Team $team): ServiceInterface\n {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n $crmService->setConfiguration($team->crm);\n if ($crmService instanceof UrlGeneratorInterface) {\n $crmService->setCrmUrlGenerator($team->crm);\n }\n\n return $crmService;\n }\n\n /**\n *\n * @return Generator<DealData>\n */\n public function getForecastData(DealsFilter $filter): Generator\n {\n $opportunities = DB::query()\n ->select([\n 'o.value',\n 'o.close_date',\n 'o.currency_code',\n 'o.is_won',\n 'o.is_closed',\n 'o.probability',\n 'o.forecast_category',\n ])\n ->from('opportunities', 'o')\n ->join('users', 'users.id', '=', 'o.user_id')\n ->join('groups', 'groups.id', '=', 'users.group_id')\n ->where('users.team_id', $filter->getTeam()->getId())\n ->where('o.close_date', '>=', $filter->getStartDate())\n ->where('o.close_date', '<=', $filter->getEndDate())\n ->where('o.currency_code', $filter->getCurrency())\n ->where('o.deleted_at', '=', null)\n ;\n\n $userUuidList = $filter->getUserUuidList();\n if (! empty($userUuidList)) {\n $userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);\n\n $opportunities->whereIn('users.uuid', $userUuidList);\n }\n\n $groupUuidList = $filter->getGroupUuidList();\n if (! empty($groupUuidList)) {\n $groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);\n\n $opportunities->whereIn('groups.uuid', $groupUuidList);\n }\n\n foreach ($opportunities->cursor() as $row) {\n yield new DealData(\n (float) $row->value,\n $row->close_date,\n ! empty($row->is_won),\n ! empty($row->is_closed),\n $row->probability ?: 0,\n $row->forecast_category ?: '',\n );\n }\n }\n\n public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection\n {\n return $user->subscriptionSets()\n ->where(static function (Eloquent\\Builder $query): void {\n $query\n ->whereNull('expired_at')\n ->orWhere('expired_at', '>=', now());\n })\n ->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n $join\n ->where('followable_type', Models\\Activity\\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)\n ->whereIn('followable_id', $opportunityIds);\n })\n ->pluck('followable_id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.24335106,"top":0.047885075,"width":0.024268618,"height":0.024740623},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3407362332434637772
|
-4167466460282672524
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20157-AJ-report-not-se Project: faVsco.js, menu
JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
RequestGenerateAskJiminnyReportJobTest
Run 'RequestGenerateAskJiminnyReportJobTest'
Debug 'RequestGenerateAskJiminnyReportJobTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
16
7
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active Ask Jiminny reports whose expiry date has passed.
*
* @return Collection<AutomatedReport>
*/
public function getExpiredActiveAskJiminnyReports(): Collection
{
return AutomatedReport::where('status', true)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereNotNull('expires_at')
->where('expires_at', '<', now()->toDateString())
->get();
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function findLatestDefaultOrFailedResult(AutomatedReport $report): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('report_id', $report->getId())
->whereIn('status', [AutomatedReportResult::STATUS_DEFAULT, AutomatedReportResult::STATUS_FAILED])
->latest()
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Restrict a query on the automated_reports table to reports the given user is allowed to see.
*
* Matches the customer-facing audience:
* - explicit user recipients (recipients.users)
* - members of any of the report's groups (Ask Jiminny reports)
*/
private function applyUserAccessScope(Builder $query, User $user): void
{
$userId = $user->getId();
$groupId = $user->getGroupId();
$query
->where('automated_reports.team_id', $user->getTeamId())
->where(function (Builder $q) use ($userId, $groupId): void {
$q->whereJsonContains('automated_reports.recipients->users', $userId);
if ($groupId !== null) {
$q->orWhere(function (Builder $sub) use ($groupId): void {
$sub->where('automated_reports.type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->whereJsonContains('automated_reports.groups', $groupId);
});
}
});
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
36
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\DealInsights;
use Doctrine\DBAL\Connection;
use Generator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Jiminny\Component\DealInsights\Forecast\DealData;
use Jiminny\Component\DealInsights\Forecast\DealsFilter;
use Jiminny\Component\DealInsights\QueryBuilder\QueryBuilder;
use Jiminny\Component\DealInsights\QueryBuilder\Visitor\QueryBuilderVisitorInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Services\Crm\IntegrationApp\DTO\Utils\UrlGeneratorInterface;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class DealsRepository implements DealsRepositoryInterface
{
private Connection $connection;
private ProviderRegistry $providerRegistry;
/**
* @var QueryBuilderVisitorInterface[]
*/
private array $visitors = [];
/**
* @param QueryBuilderVisitorInterface[] $visitors
*/
public function __construct(Connection $connection, ProviderRegistry $crmProviderRegistry, array $visitors = [])
{
$this->connection = $connection;
$this->providerRegistry = $crmProviderRegistry;
foreach ($visitors as $visitor) {
$this->visitors[$visitor->getIdentifier()] = $visitor;
}
}
public function getDeals(CriteriaInterface $criteria): array
{
$context = $criteria->getContext();
$team = $context->getTeam();
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$this->visit($qb, $criteria);
return $this->execute($team, $crmService, $qb);
}
public function getDeal(Team $team, int $id): array
{
$crmService = $this->getCrmService($team);
$qb = $this->createQueryBuilder(QueryBuilder::REALM_DEALS);
$qb = $this->getSearchSelectAndWhereClauses($qb);
$qb->andWhere('opp.id = :id')->setParameter('id', $id);
return $this->execute($team, $crmService, $qb);
}
public function getCrmFieldData(array $crmFields, int $crmId, array $opportunityIds = [])
{
$qb = new QueryBuilder($this->connection);
$qb
->select('f.id', 'f.crm_provider_id AS field_name', 'f.label', 'fd.object_id AS dealId', 'fd.value')
->from('crm_fields', 'f')
->join('f', 'crm_field_data', 'fd', 'fd.crm_field_id = f.id')
->where('f.crm_configuration_id = :crm')
->andWhere('f.object_type = :type')
->andWhere('fd.object_id IN (' . implode(',', $opportunityIds) . ')')
->orderBy('fd.object_id', 'ASC')
->addOrderBy('fd.updated_at', 'ASC')
->setParameter('type', Field::OBJECT_OPPORTUNITY)
->setParameter('crm', $crmId)
;
if (! empty($crmFields)) {
$fields = array_map(fn ($value): string => '"' . $value . '"', $crmFields);
$qb->andWhere('f.crm_provider_id IN (' . implode(',', $fields) . ')');
}
return $qb->executeQuery()->fetchAllAssociative();
}
public function getTotalsInDefaultCurrency(CriteriaInterface $criteria): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAssociative();
}
public function getTotals(CriteriaInterface $criteria, string $defaultCurrency): array
{
$qb = $this->createQueryBuilder(QueryBuilder::REALM_TOTALS);
$qb
->select('COALESCE(opp.currency_code, "' . $defaultCurrency . '") AS currency')
->addSelect('SUM(opp.value) as total')
->addSelect('count(*) as `count`')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not include deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'))
->groupBy('currency')
;
$this->visit($qb, $criteria);
return $qb->executeQuery()->fetchAllAssociative();
}
public function getDealActivities(CriteriaInterface $criteria): array
{
$qb = Activity::with(['participants', 'user'])
->where('opportunity_id', $criteria->getOpportunityId())
->whereDate('actual_start_time', '>=', $criteria->getPeriod()->getStartDate())
->whereDate('actual_start_time', '<=', $criteria->getPeriod()->getEndDate())
->orderBy($criteria->getSortBy(), $criteria->getSortDirection())
;
// Should we filter activities by criteria? It's intended to filter deals.
return $qb->get()->all();
}
public function getStages(CriteriaInterface $criteria): array
{
$qb = new QueryBuilder($this->connection);
$qb
->select('id', 'label', 'sequence')
->from('stages', 's')
->where('crm_configuration_id = :crm_configuration_id')
->andWhere('type = :type')
->orderBy('sequence', 'ASC')
->setParameter('crm_configuration_id', $criteria->getContext()->getTeam()->getCrmConfiguration()->getId())
->setParameter('type', Stage::TYPE_OPPORTUNITY);
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$result[$row['id']] = [
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
public function getConfigurationStages(Configuration $configuration): Collection
{
return $configuration
->stages()
->where('type', Stage::TYPE_OPPORTUNITY)
->get();
}
public function getPipelineData(Configuration $crm): array
{
$qb = new QueryBuilder($this->connection);
$provider = $crm->provider;
$qb
->select('s.label', 's.crm_provider_id', 's.sequence', 'bps.business_process_id AS pipeline_id')
->from('stages', 's')
->join('s', 'business_process_stages', 'bps', 's.id=bps.stage_id')
->where('s.crm_configuration_id = :crm_configuration_id')
->andWhere('s.type = :type')
->orderBy('bps.business_process_id', 'ASC')
->addOrderBy('s.sequence', 'ASC')
->setParameter('crm_configuration_id', $crm->id)
->setParameter('type', Stage::TYPE_OPPORTUNITY)
;
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$value = $provider === Configuration::PROVIDER_SALESFORCE ? $row['label'] : $row['crm_provider_id'];
$result[$row['pipeline_id']][] = [
'value' => $value,
'label' => $row['label'],
'sequence' => $row['sequence'],
];
}
return $result;
}
private function createQueryBuilder(string $realm): QueryBuilder
{
return (new QueryBuilder($this->connection))
->setRealm($realm)
->from('opportunities', 'opp')
->leftJoin('opp', 'record_types', 'rt', 'opp.record_type_id = rt.id')
->leftJoin('opp', 'users', 'usr', 'opp.user_id = usr.id')
->leftJoin('opp', 'accounts', 'acc', 'opp.account_id = acc.id')
;
}
/**
* Applies all applicable visitors and returns the IDs of the executed ones
*
* @return string[]
*/
private function visit(QueryBuilder $queryBuilder, CriteriaInterface $criteria): array
{
$queryVisitors = [];
foreach ($this->visitors as $visitor) {
if ($visitor->isSatisfiedBy($criteria, $queryBuilder->getRealm())) {
$visitor->visit($queryBuilder, $criteria);
$queryVisitors[] = $visitor->getIdentifier();
}
}
return $queryVisitors;
}
private function hydrateStages(array $deals): array
{
foreach ($this->fetchStages(array_keys($deals)) as $stage) {
$oppId = (int) $stage['opportunity_id'];
if (! isset($deals[$oppId])) {
continue; // or throw??!
}
$deals[$oppId]['stages'][] = [
'id' => $stage['stage_id'],
'name' => $stage['label'],
'enteredAt' => $stage['created_at'],
];
}
return $deals;
}
/**
* @param int[] $dealIds
*/
private function fetchStages(array $dealIds): array
{
if (empty($dealIds)) {
return [];
}
$qb = new QueryBuilder($this->connection);
$qb
->select('os.opportunity_id', 's.id AS stage_id', 's.label', 's.created_at')
->from('opportunity_stages', 'os')
->leftJoin('os', 'stages', 's', 'os.stage_id=s.id')
->where($qb->expr()->in('os.opportunity_id', $dealIds))
->orderBy('os.opportunity_id', 'ASC')
->addOrderBy('s.created_at', 'ASC')
;
return $qb->executeQuery()->fetchAllAssociative();
}
private function execute(Team $team, ServiceInterface $crmService, QueryBuilder $qb): array
{
$result = [];
foreach ($qb->executeQuery()->fetchAllAssociative() as $row) {
$data = [
'uuid' => RequiresUUID::toNormal($row['uuid']),
'name' => $row['name'],
'url' => $crmService->generateProviderUrl($row['opp_provider_id'], 'opportunity'),
'account' => [
'name' => $row['acc_name'],
'url' => $crmService->generateProviderUrl(
providerId: $row['acc_provider_id'],
objectType: $row['acc_is_internal'] ? 'internal-account' : 'account'
),
],
'owner' => null,
'rawValue' => [
'amount' => (float) $row['value'],
'currency' => $row['currency_code'],
],
'value' => formatOpportunityValue((float) $row['value'], $row['currency_code']),
'openDate' => $row['remotely_created_at'] ?? null,
'closeDate' => $row['close_date'] ?? null,
'stages' => [],
'currentPipelineId' => $row['pipeline_id'],
'currentStage' => [
'id' => $row['stage_id'],
'enteredAt' => $row['stage_updated_at'],
],
'currentStageUpdatedAt' => $row['stage_updated_at'],
'isClosed' => (bool) $row['is_closed'],
'isWon' => (bool) $row['is_won'],
];
if (isset($row['owner_uuid'])) {
$data['owner'] = [
'uuid' => RequiresUUID::toNormal($row['owner_uuid']),
'name' => $row['owner_name'],
'photoUrl' => $row['owner_photo'] === null
? null
: client_cdn($row['owner_photo'], $team),
'id' => $row['owner_id'],
'job' => $row['owner_job'],
];
}
$result[(int) $row['opp_id']] = $data;
}
return $this->hydrateStages($result);
}
private function getSearchSelectAndWhereClauses(QueryBuilder $queryBuilder): QueryBuilder
{
$qb = clone $queryBuilder;
$qb->leftJoin('usr', 'job_titles', 'jt', 'usr.job_title_id = jt.id');
$qb
->select(...[
'opp.id as opp_id',
'opp.uuid',
'opp.name',
'opp.value',
'opp.currency_code',
'opp.close_date',
'opp.remotely_created_at',
'opp.is_closed',
'opp.is_won',
])
->addSelect(...[
'usr.uuid as owner_uuid',
'usr.name AS owner_name',
'usr.photo_path as owner_photo',
'usr.id AS owner_id',
'jt.name as owner_job',
])
->addSelect('opp.stage_id', 'opp.stage_updated_at')
->addSelect(...[
'acc.name AS acc_name',
'acc.is_internal as acc_is_internal',
'opp.stage_updated_at',
'acc.crm_provider_id AS acc_provider_id',
'opp.crm_provider_id AS opp_provider_id',
])
->addSelect('rt.business_process_id AS pipeline_id')
->where($qb->expr()->isNotNull('opp.user_id')) // we should not display deals owned by external users
->andWhere($qb->expr()->isNull('opp.deleted_at'));
return $qb;
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws SocialAccountTokenInvalidException
*/
private function getCrmService(Team $team): ServiceInterface
{
$crmService = $this->providerRegistry->get($team->crm->provider);
$crmService->setConfiguration($team->crm);
if ($crmService instanceof UrlGeneratorInterface) {
$crmService->setCrmUrlGenerator($team->crm);
}
return $crmService;
}
/**
*
* @return Generator<DealData>
*/
public function getForecastData(DealsFilter $filter): Generator
{
$opportunities = DB::query()
->select([
'o.value',
'o.close_date',
'o.currency_code',
'o.is_won',
'o.is_closed',
'o.probability',
'o.forecast_category',
])
->from('opportunities', 'o')
->join('users', 'users.id', '=', 'o.user_id')
->join('groups', 'groups.id', '=', 'users.group_id')
->where('users.team_id', $filter->getTeam()->getId())
->where('o.close_date', '>=', $filter->getStartDate())
->where('o.close_date', '<=', $filter->getEndDate())
->where('o.currency_code', $filter->getCurrency())
->where('o.deleted_at', '=', null)
;
$userUuidList = $filter->getUserUuidList();
if (! empty($userUuidList)) {
$userUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $userUuidList);
$opportunities->whereIn('users.uuid', $userUuidList);
}
$groupUuidList = $filter->getGroupUuidList();
if (! empty($groupUuidList)) {
$groupUuidList = array_map(fn ($uuid) => RequiresUUID::toOptimized($uuid), $groupUuidList);
$opportunities->whereIn('groups.uuid', $groupUuidList);
}
foreach ($opportunities->cursor() as $row) {
yield new DealData(
(float) $row->value,
$row->close_date,
! empty($row->is_won),
! empty($row->is_closed),
$row->probability ?: 0,
$row->forecast_category ?: '',
);
}
}
public function getUserOpportunitySubscriptions(User $user, array $opportunityIds): Collection
{
return $user->subscriptionSets()
->where(static function (Eloquent\Builder $query): void {
$query
->whereNull('expired_at')
->orWhere('expired_at', '>=', now());
})
->join('activity_subscriptions', function (Builder $join) use ($opportunityIds) {
$join
->on('subscription_set_id', '=', 'activity_subscription_sets.id');
$join
->where('followable_type', Models\Activity\Subscription::FOLLOWABLE_TYPE_OPPORTUNITY)
->whereIn('followable_id', $opportunityIds);
})
->pluck('followable_id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
72937
|
|
72936
|
NULL
|
0
|
2026-04-23T06:24:30.435014+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776925470435_m1.jpg...
|
PhpStorm
|
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
AutomatedReportsRepository.php, class
AutomatedRep AutomatedReportsRepository.php, class
AutomatedReportsRepository.php, class
243
243
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
Merge Usages from the Same Line
Show Read Access
Show Write Access
Show Results In Generated Code
Show Import Statements
Show $this/self references
Show Options
Preview Source
Project Files
Usages
Settings…
Repositories/AutomatedReportsRepository.php
Method \Jiminny\Repositories\AutomatedReportsRepository.applyUserAccessScope AutomatedReportsRepository .../app/Repositories
1 usage
Open in Find Tool Window...
|
[{"role":"AXCell","text":& [{"role":"AXCell","text":"AutomatedReportsRepository.php, class","depth":4,"role_description":"cell"},{"role":"AXStaticText","text":"AutomatedReportsRepository.php, class","depth":5,"role_description":"text"},{"role":"AXCell","text":"243","depth":4,"role_description":"cell"},{"role":"AXStaticText","text":"243","depth":5,"role_description":"text"},{"role":"AXCell","text":"->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))","depth":4,"role_description":"cell"},{"role":"AXStaticText","text":"->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))","depth":5,"role_description":"text"},{"role":"AXButton","text":"Merge Usages from the Same Line","depth":2,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Show Read Access","depth":2,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Show Write Access","depth":2,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Show Results In Generated Code","depth":2,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Show Import Statements","depth":2,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Show $this/self references","depth":2,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Show Options","depth":2,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Preview Source","depth":2,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Project Files","depth":2,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Usages","depth":1,"role_description":"text"},{"role":"AXButton","text":"Settings…","depth":2,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Repositories/AutomatedReportsRepository.php","depth":1,"help_text":"Repositories/AutomatedReportsRepository.php","role_description":"text"},{"role":"AXStaticText","text":"Method \\Jiminny\\Repositories\\AutomatedReportsRepository.applyUserAccessScope AutomatedReportsRepository .../app/Repositories","depth":1,"role_description":"text"},{"role":"AXStaticText","text":"1 usage","depth":1,"role_description":"text"},{"role":"AXButton","text":"Open in Find Tool Window","depth":2,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-4475301662402985730
|
-585724057270845192
|
click
|
accessibility
|
NULL
|
AutomatedReportsRepository.php, class
AutomatedRep AutomatedReportsRepository.php, class
AutomatedReportsRepository.php, class
243
243
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
->where(fn (Builder $q) => $this->applyUserAccessScope($q, $user))
Merge Usages from the Same Line
Show Read Access
Show Write Access
Show Results In Generated Code
Show Import Statements
Show $this/self references
Show Options
Preview Source
Project Files
Usages
Settings…
Repositories/AutomatedReportsRepository.php
Method \Jiminny\Repositories\AutomatedReportsRepository.applyUserAccessScope AutomatedReportsRepository .../app/Repositories
1 usage
Open in Find Tool Window...
|
NULL
|
|
72851
|
NULL
|
0
|
2026-04-23T06:19:17.901985+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776925157901_m1.jpg...
|
PhpStorm
|
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Open…
⌘O
Clone Repository…
Open Projects
faVsco.js Open…
⌘O
Clone Repository…
Open Projects
faVsco.js
~/jiminny/app
JY-20157-AJ-report-not-send-notification
Recent Projects
infrastructure
~/jiminny/infrastructure
develop
vue-components
~/jiminny/vue-components
master
SC
~/DEV/Projects/SC...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Open…","depth":5,"bounds":{"left":0.0,"top":0.0,"width":0.08055556,"height":0.024444444},"role_description":"text"},{"role":"AXStaticText","text":"⌘O","depth":5,"bounds":{"left":0.0,"top":0.0,"width":0.0055555557,"height":0.024444444},"role_description":"text"},{"role":"AXStaticText","text":"Clone Repository…","depth":5,"bounds":{"left":0.0,"top":0.0,"width":0.08055556,"height":0.024444444},"role_description":"text"},{"role":"AXStaticText","text":"Open Projects","depth":5,"role_description":"text"},{"role":"AXStaticText","text":"faVsco.js","depth":5,"role_description":"text"},{"role":"AXStaticText","text":"~/jiminny/app","depth":5,"role_description":"text"},{"role":"AXStaticText","text":"JY-20157-AJ-report-not-send-notification","depth":5,"role_description":"text"},{"role":"AXStaticText","text":"Recent Projects","depth":5,"role_description":"text"},{"role":"AXStaticText","text":"infrastructure","depth":5,"role_description":"text"},{"role":"AXStaticText","text":"~/jiminny/infrastructure","depth":5,"role_description":"text"},{"role":"AXStaticText","text":"develop","depth":5,"role_description":"text"},{"role":"AXStaticText","text":"vue-components","depth":5,"role_description":"text"},{"role":"AXStaticText","text":"~/jiminny/vue-components","depth":5,"role_description":"text"},{"role":"AXStaticText","text":"master","depth":5,"role_description":"text"},{"role":"AXStaticText","text":"SC","depth":5,"role_description":"text"},{"role":"AXStaticText","text":"~/DEV/Projects/SC","depth":5,"role_description":"text"}]...
|
2079049776495123607
|
2177465869071140649
|
click
|
accessibility
|
NULL
|
Open…
⌘O
Clone Repository…
Open Projects
faVsco.js Open…
⌘O
Clone Repository…
Open Projects
faVsco.js
~/jiminny/app
JY-20157-AJ-report-not-send-notification
Recent Projects
infrastructure
~/jiminny/infrastructure
develop
vue-components
~/jiminny/vue-components
master
SC
~/DEV/Projects/SC...
|
72848
|
|
72850
|
NULL
|
0
|
2026-04-23T06:19:15.429459+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776925155429_m2.jpg...
|
PhpStorm
|
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Open…
⌘O
Clone Repository…
Open Projects
faVsco.js Open…
⌘O
Clone Repository…
Open Projects
faVsco.js
~/jiminny/app
JY-20157-AJ-report-not-send-notification
Recent Projects
infrastructure
~/jiminny/infrastructure
develop...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Open…","depth":5,"bounds":{"left":0.27027926,"top":1.0,"width":0.03856383,"height":0.0},"role_description":"text"},{"role":"AXStaticText","text":"⌘O","depth":5,"bounds":{"left":0.27027926,"top":1.0,"width":0.0026595744,"height":0.0},"role_description":"text"},{"role":"AXStaticText","text":"Clone Repository…","depth":5,"bounds":{"left":0.27027926,"top":1.0,"width":0.03856383,"height":0.0},"role_description":"text"},{"role":"AXStaticText","text":"Open Projects","depth":5,"role_description":"text"},{"role":"AXStaticText","text":"faVsco.js","depth":5,"role_description":"text"},{"role":"AXStaticText","text":"~/jiminny/app","depth":5,"role_description":"text"},{"role":"AXStaticText","text":"JY-20157-AJ-report-not-send-notification","depth":5,"role_description":"text"},{"role":"AXStaticText","text":"Recent Projects","depth":5,"role_description":"text"},{"role":"AXStaticText","text":"infrastructure","depth":5,"role_description":"text"},{"role":"AXStaticText","text":"~/jiminny/infrastructure","depth":5,"role_description":"text"},{"role":"AXStaticText","text":"develop","depth":5,"role_description":"text"}]...
|
-3700734768434301242
|
2599290197674608481
|
visual_change
|
accessibility
|
NULL
|
Open…
⌘O
Clone Repository…
Open Projects
faVsco.js Open…
⌘O
Clone Repository…
Open Projects
faVsco.js
~/jiminny/app
JY-20157-AJ-report-not-send-notification
Recent Projects
infrastructure
~/jiminny/infrastructure
develop...
|
72849
|
|
72786
|
NULL
|
0
|
2026-04-23T06:14:12.688692+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776924852688_m2.jpg...
|
Slack
|
engineering (Channel) - Jiminny Inc - Slack
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
c-learning-people
confusion-clinic
curiosity_lab
deal-insights-dev
engineering
frontend
general
infra-changes
jiminny-bg
people-with-copilot-licences
people-with-zoom-phone-licences
platform-team
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Petko Kashinski
Stoyan Tomov
Aneliya Angelova
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Nikolay Yankov
Nikolay Nikolov
Mario Georgiev
Todor Stamatov
Gabriela Dureva
Vasil Vasilev
Galya Dimitrova
Stefka Stoyanova
Stoyan Tanev
Nikolay Ivanov
Jira Cloud
Toast
Messages
Messages
Canvas
Canvas
Files
Files
More
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Ilian Kyuchukov
Yesterday at 9:31:41 AM
9:31 AM
Has there been any changes related to Hubspot?
The following Sentry has been escalating over the night with 14k events:
https://jiminny.sentry.io/issues/7007366572/?project=82419&referrer=metric-issue-contributing-issues-issue-stream
https://jiminny.sentry.io/issues/7007366572/?project=82419&referrer=metric-issue-contributing-issues-issue-stream
SevenShores\Hubspot\Exceptions\BadRequest
SevenShores\Hubspot\Exceptions\BadRequest
/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php in Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::executeSearchRequest
Client error: `POST
https://api.hubapi.com/crm/v3/objects/contact/search
` resulted in a `429 Too Many Requests` response:
{"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT","correlationId":"019db2b6-c (truncated...
Events:
14202
State:
Ongoing
First Seen:
2025-11-08
Resolve
Resolve
Archive
Archive
Select Assignee...
See more
Added by
Sentry
Sentry
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Nikolay Nikolov
Yesterday at 9:40:55 AM
9:40 AM
We have changed processing of webhooks from 30 min to 5 min smaller batches
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Jump to date
New
CircleCI
APP
Today at 8:54:46 AM
8:54 AM
1 PR with vulnerability fixes is ready for review
Please take a look at the code and confirm that everything works properly on your local machine or on a planet environment
Pull requests
(jiminny/app)
•
#11975 fix(security): npm dependency updates – 2026-04-16
#11975 fix(security): npm dependency updates – 2026-04-16
(
secfix/npm-20260416
)
View workflow run
View workflow run
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Conversation marked as unread from this message
Channel engineering...
|
[{"role":"AXPopUpButton","text [{"role":"AXPopUpButton","text":"Switch workspaces… (Jiminny Inc) Has new messages","depth":14,"bounds":{"left":0.0056515955,"top":0.058260176,"width":0.011968086,"height":0.028731046},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"Home","depth":14,"bounds":{"left":0.0029920214,"top":0.10055866,"width":0.017287234,"height":0.054269753},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Home","depth":16,"bounds":{"left":0.0066489363,"top":0.13806863,"width":0.009973404,"height":0.0103751},"role_description":"text"},{"role":"AXRadioButton","text":"DMs","depth":14,"bounds":{"left":0.0029920214,"top":0.15482841,"width":0.017287234,"height":0.054269753},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"DMs","depth":16,"bounds":{"left":0.0076462766,"top":0.19233839,"width":0.007978723,"height":0.0103751},"role_description":"text"},{"role":"AXRadioButton","text":"Activity","depth":14,"bounds":{"left":0.0029920214,"top":0.20909816,"width":0.017287234,"height":0.054269753},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Activity","depth":16,"bounds":{"left":0.004986702,"top":0.24660814,"width":0.012965426,"height":0.0103751},"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":14,"bounds":{"left":0.0029920214,"top":0.26336792,"width":0.017287234,"height":0.054269753},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":16,"bounds":{"left":0.0076462766,"top":0.3008779,"width":0.0076462766,"height":0.0103751},"role_description":"text"},{"role":"AXRadioButton","text":"Later","depth":14,"bounds":{"left":0.0029920214,"top":0.31763768,"width":0.017287234,"height":0.054269753},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Later","depth":16,"bounds":{"left":0.00731383,"top":0.35514766,"width":0.008643617,"height":0.0103751},"role_description":"text"},{"role":"AXRadioButton","text":"More…","depth":14,"bounds":{"left":0.0029920214,"top":0.3719074,"width":0.017287234,"height":0.054269753},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More","depth":16,"bounds":{"left":0.006981383,"top":0.4094174,"width":0.008976064,"height":0.0103751},"role_description":"text"},{"role":"AXStaticText","text":"Unreads","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Threads","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Huddles","depth":21,"bounds":{"left":0.036901597,"top":0.09177973,"width":0.017952127,"height":0.011173184},"role_description":"text"},{"role":"AXStaticText","text":"Drafts & sent","depth":21,"bounds":{"left":0.036901597,"top":0.11093376,"width":0.028922873,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Directories","depth":21,"bounds":{"left":0.036901597,"top":0.13328013,"width":0.023936171,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"jiminny-x-integration-app","depth":23,"bounds":{"left":0.042220745,"top":0.22745411,"width":0.043882977,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"platform-inner-team","depth":23,"bounds":{"left":0.042220745,"top":0.24980047,"width":0.044215426,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"ai-chapter","depth":23,"bounds":{"left":0.042220745,"top":0.30247405,"width":0.022273935,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"alerts","depth":23,"bounds":{"left":0.042220745,"top":0.32482043,"width":0.011968086,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"backend","depth":23,"bounds":{"left":0.042220745,"top":0.3471668,"width":0.018284574,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"c-learning-people","depth":23,"bounds":{"left":0.042220745,"top":0.36951315,"width":0.038231384,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"confusion-clinic","depth":23,"bounds":{"left":0.042220745,"top":0.39185953,"width":0.034242023,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"curiosity_lab","depth":23,"bounds":{"left":0.042220745,"top":0.4142059,"width":0.027593086,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"deal-insights-dev","depth":23,"bounds":{"left":0.042220745,"top":0.4365523,"width":0.03723404,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"engineering","depth":23,"bounds":{"left":0.042220745,"top":0.45889863,"width":0.025598405,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"frontend","depth":23,"bounds":{"left":0.042220745,"top":0.481245,"width":0.018949468,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"general","depth":23,"bounds":{"left":0.042220745,"top":0.50359136,"width":0.015957447,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"infra-changes","depth":23,"bounds":{"left":0.042220745,"top":0.52593774,"width":0.029587766,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"jiminny-bg","depth":23,"bounds":{"left":0.042220745,"top":0.5482841,"width":0.022938829,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"people-with-copilot-licences","depth":23,"bounds":{"left":0.042220745,"top":0.5706305,"width":0.045212764,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"people-with-zoom-phone-licences","depth":23,"bounds":{"left":0.042220745,"top":0.59297687,"width":0.045877658,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"platform-team","depth":23,"bounds":{"left":0.042220745,"top":0.61532325,"width":0.03125,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"platform-tickets","depth":23,"bounds":{"left":0.042220745,"top":0.63766956,"width":0.034906916,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"product_launches","depth":23,"bounds":{"left":0.042220745,"top":0.66001594,"width":0.03856383,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"random","depth":23,"bounds":{"left":0.042220745,"top":0.6823623,"width":0.01662234,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"releases","depth":23,"bounds":{"left":0.042220745,"top":0.7047087,"width":0.01761968,"height":0.0047885077},"role_description":"text"},{"role":"AXStaticText","text":"sofia-office","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.024268618,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"support","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.016954787,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"thank-yous","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.024268618,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"the_people_of_jiminny","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.04488032,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Petko Kashinski","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.034242023,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Stoyan Tomov","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.030585106,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.03756649,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.03756649,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":",","depth":23,"bounds":{"left":0.07945479,"top":0.7086991,"width":0.0063164895,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Yankov","depth":23,"bounds":{"left":0.08211436,"top":0.7086991,"width":0.014295213,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":",","depth":23,"bounds":{"left":0.09607713,"top":0.7086991,"width":0.0003324468,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Steliyan Georgiev","depth":23,"bounds":{"left":0.09607713,"top":0.7086991,"width":0.0003324468,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Yankov","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.032912236,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Nikolov","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.034242023,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Mario Georgiev","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.033909574,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Todor Stamatov","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.034242023,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Gabriela Dureva","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.03523936,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Vasil Vasilev","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.026263298,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Galya Dimitrova","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.034906916,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Stefka Stoyanova","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.03756649,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Stoyan Tanev","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.028922873,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Ivanov","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.031914894,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Jira Cloud","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.021609042,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Toast","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.011635638,"height":0.0007980846},"role_description":"text"},{"role":"AXRadioButton","text":"Messages","depth":18,"bounds":{"left":0.10206117,"top":0.09177973,"width":0.030585106,"height":0.030327214},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Messages","depth":20,"bounds":{"left":0.111369684,"top":0.10055866,"width":0.01861702,"height":0.012769354},"role_description":"text"},{"role":"AXRadioButton","text":"Canvas","depth":18,"bounds":{"left":0.13397606,"top":0.09177973,"width":0.025930852,"height":0.030327214},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Canvas","depth":20,"bounds":{"left":0.14328457,"top":0.10055866,"width":0.013962766,"height":0.012769354},"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":18,"bounds":{"left":0.16090426,"top":0.09177973,"width":0.020944148,"height":0.030327214},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":20,"bounds":{"left":0.17021276,"top":0.10055866,"width":0.008976064,"height":0.012769354},"role_description":"text"},{"role":"AXRadioButton","text":"More","depth":19,"bounds":{"left":0.18317819,"top":0.09177973,"width":0.020279255,"height":0.030327214},"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Add and Edit Channel Tabs","depth":18,"bounds":{"left":0.203125,"top":0.09177973,"width":0.010970744,"height":0.030327214},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Canvas","depth":18,"bounds":{"left":0.096409574,"top":0.0518755,"width":0.015625,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"List","depth":18,"bounds":{"left":0.096409574,"top":0.0518755,"width":0.0076462766,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Folder","depth":18,"bounds":{"left":0.096409574,"top":0.0518755,"width":0.013962766,"height":0.0007980846},"role_description":"text"},{"role":"AXPopUpButton","text":"Jump to date","depth":24,"bounds":{"left":0.14660904,"top":0.12689546,"width":0.032579787,"height":0.022346368},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Ilian Kyuchukov","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":25,"role_description":"text"},{"role":"AXLink","text":"Yesterday at 9:31:41 AM","depth":25,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9:31 AM","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"Has there been any changes related to Hubspot?","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"The following Sentry has been escalating over the night with 14k events:","depth":25,"role_description":"text"},{"role":"AXLink","text":"https://jiminny.sentry.io/issues/7007366572/?project=82419&referrer=metric-issue-contributing-issues-issue-stream","depth":25,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"https://jiminny.sentry.io/issues/7007366572/?project=82419&referrer=metric-issue-contributing-issues-issue-stream","depth":26,"role_description":"text"},{"role":"AXLink","text":"SevenShores\\Hubspot\\Exceptions\\BadRequest","depth":28,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"SevenShores\\Hubspot\\Exceptions\\BadRequest","depth":29,"role_description":"text"},{"role":"AXStaticText","text":"/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php in Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest","depth":27,"role_description":"text"},{"role":"AXStaticText","text":"Client error: `POST","depth":29,"role_description":"text"},{"role":"AXLink","text":"https://api.hubapi.com/crm/v3/objects/contact/search","depth":29,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"` resulted in a `429 Too Many Requests` response:","depth":29,"role_description":"text"},{"role":"AXStaticText","text":"{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019db2b6-c (truncated...","depth":29,"bounds":{"left":0.1263298,"top":0.11572227,"width":0.091090426,"height":0.039106146},"role_description":"text"},{"role":"AXStaticText","text":"Events:","depth":27,"bounds":{"left":0.12333777,"top":0.1763767,"width":0.014960106,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"14202","depth":27,"bounds":{"left":0.13796543,"top":0.1763767,"width":0.012632979,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.1505984,"top":0.1763767,"width":0.0013297872,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.15159574,"top":0.1763767,"width":0.0013297872,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"State:","depth":27,"bounds":{"left":0.15259309,"top":0.1763767,"width":0.013297873,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"Ongoing","depth":27,"bounds":{"left":0.16589096,"top":0.1763767,"width":0.01662234,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.18218085,"top":0.1763767,"width":0.0013297872,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.18317819,"top":0.1763767,"width":0.0013297872,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"First Seen:","depth":27,"bounds":{"left":0.18450798,"top":0.1763767,"width":0.022273935,"height":0.012769354},"role_description":"text"},{"role":"AXStaticText","text":"2025-11-08","depth":27,"bounds":{"left":0.12333777,"top":0.1763767,"width":0.09507979,"height":0.030327214},"role_description":"text"},{"role":"AXButton","text":"Resolve","depth":27,"bounds":{"left":0.12333777,"top":0.21787709,"width":0.020944148,"height":0.022346368},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Resolve","depth":29,"bounds":{"left":0.1263298,"top":0.22186752,"width":0.014960106,"height":0.012769354},"role_description":"text"},{"role":"AXButton","text":"Archive","depth":27,"bounds":{"left":0.14660904,"top":0.21787709,"width":0.020944148,"height":0.022346368},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Archive","depth":29,"bounds":{"left":0.14960106,"top":0.22186752,"width":0.014960106,"height":0.012769354},"role_description":"text"},{"role":"AXComboBox","text":"Select Assignee...","depth":28,"bounds":{"left":0.12333777,"top":0.24660814,"width":0.0631649,"height":0.022346368},"role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"See more","depth":26,"bounds":{"left":0.12333777,"top":0.2753392,"width":0.020611702,"height":0.017557861},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Added by","depth":27,"bounds":{"left":0.12333777,"top":0.29608938,"width":0.01761968,"height":0.011173184},"role_description":"text"},{"role":"AXLink","text":"Sentry","depth":27,"bounds":{"left":0.14095744,"top":0.29608938,"width":0.011303191,"height":0.011173184},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Sentry","depth":28,"bounds":{"left":0.14095744,"top":0.29608938,"width":0.011303191,"height":0.011173184},"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":27,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Nikolay Nikolov","depth":25,"bounds":{"left":0.11801862,"top":0.31763768,"width":0.035904255,"height":0.017557861},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":25,"bounds":{"left":0.15359043,"top":0.31923383,"width":0.0026595744,"height":0.014365523},"role_description":"text"},{"role":"AXLink","text":"Yesterday at 9:40:55 AM","depth":25,"bounds":{"left":0.15625,"top":0.3216281,"width":0.015292553,"height":0.011173184},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9:40 AM","depth":26,"bounds":{"left":0.15625,"top":0.3216281,"width":0.015292553,"height":0.011173184},"role_description":"text"},{"role":"AXStaticText","text":"We have changed processing of webhooks from 30 min to 5 min smaller batches","depth":26,"bounds":{"left":0.11801862,"top":0.3367917,"width":0.1043883,"height":0.031923383},"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":27,"bounds":{"left":0.13730054,"top":0.30407023,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":27,"bounds":{"left":0.14793883,"top":0.30407023,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":27,"bounds":{"left":0.15857713,"top":0.30407023,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":27,"bounds":{"left":0.16921543,"top":0.30407023,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":27,"bounds":{"left":0.17985372,"top":0.30407023,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":27,"bounds":{"left":0.22340426,"top":0.30407023,"width":0.0003324468,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":27,"bounds":{"left":0.22340426,"top":0.30407023,"width":0.0003324468,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":27,"bounds":{"left":0.22340426,"top":0.30407023,"width":0.0003324468,"height":0.025538707},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Jump to date","depth":24,"bounds":{"left":0.15026596,"top":0.38467678,"width":0.025265958,"height":0.022346368},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"New","depth":24,"bounds":{"left":0.21343085,"top":0.3886672,"width":0.00930851,"height":0.012769354},"role_description":"text"},{"role":"AXButton","text":"CircleCI","depth":25,"bounds":{"left":0.11801862,"top":0.41580206,"width":0.01761968,"height":0.017557861},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"APP","depth":25,"bounds":{"left":0.13796543,"top":0.4197925,"width":0.0066489363,"height":0.009577015},"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"bounds":{"left":0.14527926,"top":0.41739824,"width":0.0026595744,"height":0.014365523},"role_description":"text"},{"role":"AXLink","text":"Today at 8:54:46 AM","depth":25,"bounds":{"left":0.14793883,"top":0.4197925,"width":0.015292553,"height":0.011173184},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8:54 AM","depth":26,"bounds":{"left":0.14793883,"top":0.4197925,"width":0.015292553,"height":0.011173184},"role_description":"text"},{"role":"AXStaticText","text":"1 PR with vulnerability fixes is ready for review","depth":26,"bounds":{"left":0.11801862,"top":0.4349561,"width":0.103390954,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"Please take a look at the code and confirm that everything works properly on your local machine or on a planet environment","depth":26,"bounds":{"left":0.11801862,"top":0.4764565,"width":0.10239362,"height":0.049481247},"role_description":"text"},{"role":"AXStaticText","text":"Pull requests","depth":26,"bounds":{"left":0.11801862,"top":0.5355148,"width":0.028590426,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"(jiminny/app)","depth":26,"bounds":{"left":0.1462766,"top":0.5355148,"width":0.029920213,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"•","depth":26,"bounds":{"left":0.11801862,"top":0.55307263,"width":0.0043218085,"height":0.014365523},"role_description":"text"},{"role":"AXLink","text":"#11975 fix(security): npm dependency updates – 2026-04-16","depth":26,"bounds":{"left":0.11801862,"top":0.55307263,"width":0.08809841,"height":0.031923383},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"#11975 fix(security): npm dependency updates – 2026-04-16","depth":27,"bounds":{"left":0.11801862,"top":0.55307263,"width":0.08809841,"height":0.031923383},"role_description":"text"},{"role":"AXStaticText","text":"(","depth":26,"bounds":{"left":0.16755319,"top":0.5706305,"width":0.0026595744,"height":0.014365523},"role_description":"text"},{"role":"AXStaticText","text":"secfix/npm-20260416","depth":27,"bounds":{"left":0.17154256,"top":0.57302475,"width":0.045545213,"height":0.011971269},"role_description":"text"},{"role":"AXStaticText","text":")","depth":26,"bounds":{"left":0.21841756,"top":0.5706305,"width":0.0013297872,"height":0.014365523},"role_description":"text"},{"role":"AXLink","text":"View workflow run","depth":26,"bounds":{"left":0.11801862,"top":0.5961692,"width":0.035904255,"height":0.012769354},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"View workflow run","depth":27,"bounds":{"left":0.11801862,"top":0.5961692,"width":0.035904255,"height":0.012769354},"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":27,"bounds":{"left":0.13696809,"top":0.40303272,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":27,"bounds":{"left":0.14760639,"top":0.40303272,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":27,"bounds":{"left":0.15824468,"top":0.40303272,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":27,"bounds":{"left":0.16888298,"top":0.40303272,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":27,"bounds":{"left":0.17952128,"top":0.40303272,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":27,"bounds":{"left":0.19015957,"top":0.40303272,"width":0.010638298,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":27,"bounds":{"left":0.20079787,"top":0.40303272,"width":0.010638298,"height":0.025538707},"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":27,"bounds":{"left":0.21143617,"top":0.40303272,"width":0.010638298,"height":0.025538707},"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"","depth":24,"bounds":{"left":0.10372341,"top":0.6272945,"width":0.118351065,"height":0.030327214},"value":"","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Conversation marked as unread from this message","depth":11,"bounds":{"left":0.0,"top":0.7126895,"width":0.02825798,"height":0.0007980846},"role_description":"text"},{"role":"AXStaticText","text":"Channel engineering","depth":11,"bounds":{"left":0.0,"top":0.7126895,"width":0.024933511,"height":0.0007980846},"role_description":"text"}]...
|
4605827099065284696
|
-6180744506879857920
|
visual_change
|
hybrid
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
c-learning-people
confusion-clinic
curiosity_lab
deal-insights-dev
engineering
frontend
general
infra-changes
jiminny-bg
people-with-copilot-licences
people-with-zoom-phone-licences
platform-team
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Petko Kashinski
Stoyan Tomov
Aneliya Angelova
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Nikolay Yankov
Nikolay Nikolov
Mario Georgiev
Todor Stamatov
Gabriela Dureva
Vasil Vasilev
Galya Dimitrova
Stefka Stoyanova
Stoyan Tanev
Nikolay Ivanov
Jira Cloud
Toast
Messages
Messages
Canvas
Canvas
Files
Files
More
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Ilian Kyuchukov
Yesterday at 9:31:41 AM
9:31 AM
Has there been any changes related to Hubspot?
The following Sentry has been escalating over the night with 14k events:
https://jiminny.sentry.io/issues/7007366572/?project=82419&referrer=metric-issue-contributing-issues-issue-stream
https://jiminny.sentry.io/issues/7007366572/?project=82419&referrer=metric-issue-contributing-issues-issue-stream
SevenShores\Hubspot\Exceptions\BadRequest
SevenShores\Hubspot\Exceptions\BadRequest
/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php in Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::executeSearchRequest
Client error: `POST
https://api.hubapi.com/crm/v3/objects/contact/search
` resulted in a `429 Too Many Requests` response:
{"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT","correlationId":"019db2b6-c (truncated...
Events:
14202
State:
Ongoing
First Seen:
2025-11-08
Resolve
Resolve
Archive
Archive
Select Assignee...
See more
Added by
Sentry
Sentry
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Nikolay Nikolov
Yesterday at 9:40:55 AM
9:40 AM
We have changed processing of webhooks from 30 min to 5 min smaller batches
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Jump to date
New
CircleCI
APP
Today at 8:54:46 AM
8:54 AM
1 PR with vulnerability fixes is ready for review
Please take a look at the code and confirm that everything works properly on your local machine or on a planet environment
Pull requests
(jiminny/app)
•
#11975 fix(security): npm dependency updates – 2026-04-16
#11975 fix(security): npm dependency updates – 2026-04-16
(
secfix/npm-20260416
)
View workflow run
View workflow run
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Conversation marked as unread from this message
Channel engineering
slackcalVIewmistonWindowHelpQ Describe what you are looking for# engineering9 24Messages©Canvase FilesMoreTimit.",Yesterday AlE_LIMIl", "correlationdunEvents: 14202 State: Ongoing First Seen: 2025-11-08DMSActivityMoreJiminny...y6d hudales→ Drafts & sentl8 Directoriesab External connections* Starredi@ liminny-x-integrati…8 olatform-inner-teamE Channelsa ai-chanter# alerts# backend# c-learning-people# confusion-clinic# curiosity lab# deal-insights-dev# engineering# frontend# generall# infra-changes# liminnv-bgA neonle-with-conilo…@ people-with-zoom-….i olatform-team# platform-tickets# product launchesst randomlNikolay Nikolov 9.40 AMWe have changed processing of webhooks from30 min to 5 min smallier batchesdircled App 8.54AM… 1 PR with vulnerahilit nxec is ready for reviewPlease take k look at the code and conhrm thatieverything works properly on your localmachine or on a planet environmentPull requests (minny aoe)•#11975 hx/securim: nom denendencvuodates - 2026-04-16secfix/nom-20260416)Message #engineering+ Aa IWork RenortAl SummaryFieldDetinitionAND alsoPostmanSource All Sourcesany ap;v Date dd / mm / yyyy • (blank = all dates) From |To --:-Only appsany app (blank = allnone skippedFTS tio: sinale words work best , "exact ohrase". termi OR term2. use AND also for required second term , times are in vour local timezone . click anv result to expandEnter a query and oress Search (or Enter).• 50#Daily - Platform • in 31 m100% LzThu 23 Apr 9:14:1421/ 04 / 2026Search...
|
NULL
|
|
72785
|
NULL
|
0
|
2026-04-23T06:14:12.683608+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-23/1776 /Users/lukas/.screenpipe/data/data/2026-04-23/1776924852683_m1.jpg...
|
Slack
|
engineering (Channel) - Jiminny Inc - Slack
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
c-learning-people
confusion-clinic
curiosity_lab
deal-insights-dev
engineering
frontend
general
infra-changes
jiminny-bg
people-with-copilot-licences
people-with-zoom-phone-licences
platform-team
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Petko Kashinski
Stoyan Tomov
Aneliya Angelova
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Nikolay Yankov
Nikolay Nikolov
Mario Georgiev
Todor Stamatov
Gabriela Dureva
Vasil Vasilev
Galya Dimitrova
Stefka Stoyanova
Stoyan Tanev
Nikolay Ivanov
Jira Cloud
Toast
Messages
Messages
Canvas
Canvas
Files
Files
More
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
CircleCI
APP
Apr 21st at 8:51:40 AM
8:51 AM
1 PR with vulnerability fixes is ready for review
Please take a look at the code and confirm that everything works properly on your local machine or on a planet environment
Pull requests
(jiminny/app)
•
#11975 fix(security): npm dependency updates – 2026-04-16
#11975 fix(security): npm dependency updates – 2026-04-16
(
secfix/npm-20260416
)
View workflow run
View workflow run
6 replies
Last reply 1 day ago
View thread
Jump to date
CircleCI
APP
Yesterday at 8:51:39 AM
8:51 AM
1 PR with vulnerability fixes is ready for review
Please take a look at the code and confirm that everything works properly on your local machine or on a planet environment
Pull requests
(jiminny/app)
•
#11975 fix(security): npm dependency updates – 2026-04-16
#11975 fix(security): npm dependency updates – 2026-04-16
(
secfix/npm-20260416
)
View workflow run
View workflow run
Ilian Kyuchukov
Yesterday at 9:31:41 AM
9:31 AM
Has there been any changes related to Hubspot?
The following Sentry has been escalating over the night with 14k events:
https://jiminny.sentry.io/issues/7007366572/?project=82419&referrer=metric-issue-contributing-issues-issue-stream
https://jiminny.sentry.io/issues/7007366572/?project=82419&referrer=metric-issue-contributing-issues-issue-stream
SevenShores\Hubspot\Exceptions\BadRequest
SevenShores\Hubspot\Exceptions\BadRequest
/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php in Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::executeSearchRequest
Client error: `POST
https://api.hubapi.com/crm/v3/objects/contact/search
` resulted in a `429 Too Many Requests` response:
{"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT","correlationId":"019db2b6-c (truncated...
Events:
14202
State:
Ongoing
First Seen:
2025-11-08
Resolve
Resolve
Archive
Archive
Select Assignee...
See more
Added by
Sentry
Sentry
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Nikolay Nikolov
Yesterday at 9:40:55 AM
9:40 AM
We have changed processing of webhooks from 30 min to 5 min smaller batches
Jump to date
New
CircleCI
APP
Today at 8:54:46 AM
8:54 AM
1 PR with vulnerability fixes is ready for review
Please take a look at the code and confirm that everything works properly on your local machine or on a planet environment
Pull requests
(jiminny/app)
•
#11975 fix(security): npm dependency updates – 2026-04-16
#11975 fix(security): npm dependency updates – 2026-04-16
(
secfix/npm-20260416
)
View workflow run
View workflow run
Conversation marked as unread from this message
Channel engineering...
|
[{"role":"AXPopUpButton","text [{"role":"AXPopUpButton","text":"Switch workspaces… (Jiminny Inc) Has new messages","depth":14,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"Home","depth":14,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Home","depth":16,"role_description":"text"},{"role":"AXRadioButton","text":"DMs","depth":14,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"DMs","depth":16,"role_description":"text"},{"role":"AXRadioButton","text":"Activity","depth":14,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Activity","depth":16,"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":14,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":16,"role_description":"text"},{"role":"AXRadioButton","text":"Later","depth":14,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Later","depth":16,"role_description":"text"},{"role":"AXRadioButton","text":"More…","depth":14,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More","depth":16,"role_description":"text"},{"role":"AXStaticText","text":"Unreads","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Threads","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Huddles","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Drafts & sent","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"Directories","depth":21,"role_description":"text"},{"role":"AXStaticText","text":"jiminny-x-integration-app","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"platform-inner-team","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"ai-chapter","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"alerts","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"backend","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"c-learning-people","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"confusion-clinic","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"curiosity_lab","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"deal-insights-dev","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"engineering","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"frontend","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"general","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"infra-changes","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"jiminny-bg","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"people-with-copilot-licences","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"people-with-zoom-phone-licences","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"platform-team","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"platform-tickets","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"product_launches","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"random","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"releases","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"sofia-office","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"support","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"thank-yous","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"the_people_of_jiminny","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Petko Kashinski","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Stoyan Tomov","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":23,"role_description":"text"},{"role":"AXStaticText","text":",","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Yankov","depth":23,"role_description":"text"},{"role":"AXStaticText","text":",","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Steliyan Georgiev","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Yankov","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Nikolov","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Mario Georgiev","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Todor Stamatov","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Gabriela Dureva","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Vasil Vasilev","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Galya Dimitrova","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Stefka Stoyanova","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Stoyan Tanev","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Ivanov","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Jira Cloud","depth":23,"role_description":"text"},{"role":"AXStaticText","text":"Toast","depth":23,"role_description":"text"},{"role":"AXRadioButton","text":"Messages","depth":18,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Messages","depth":20,"role_description":"text"},{"role":"AXRadioButton","text":"Canvas","depth":18,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Canvas","depth":20,"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":18,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":20,"role_description":"text"},{"role":"AXRadioButton","text":"More","depth":19,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Add and Edit Channel Tabs","depth":18,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Canvas","depth":18,"role_description":"text"},{"role":"AXStaticText","text":"List","depth":18,"role_description":"text"},{"role":"AXStaticText","text":"Folder","depth":18,"role_description":"text"},{"role":"AXPopUpButton","text":"Jump to date","depth":24,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"CircleCI","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"APP","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"role_description":"text"},{"role":"AXLink","text":"Apr 21st at 8:51:40 AM","depth":25,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8:51 AM","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"1 PR with vulnerability fixes is ready for review","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"Please take a look at the code and confirm that everything works properly on your local machine or on a planet environment","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"Pull requests","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"(jiminny/app)","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"•","depth":26,"role_description":"text"},{"role":"AXLink","text":"#11975 fix(security): npm dependency updates – 2026-04-16","depth":26,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"#11975 fix(security): npm dependency updates – 2026-04-16","depth":27,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"secfix/npm-20260416","depth":27,"role_description":"text"},{"role":"AXStaticText","text":")","depth":26,"role_description":"text"},{"role":"AXLink","text":"View workflow run","depth":26,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"View workflow run","depth":27,"role_description":"text"},{"role":"AXButton","text":"6 replies","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Last reply 1 day ago","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"View thread","depth":26,"role_description":"text"},{"role":"AXPopUpButton","text":"Jump to date","depth":24,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"CircleCI","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"APP","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"role_description":"text"},{"role":"AXLink","text":"Yesterday at 8:51:39 AM","depth":25,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8:51 AM","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"1 PR with vulnerability fixes is ready for review","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"Please take a look at the code and confirm that everything works properly on your local machine or on a planet environment","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"Pull requests","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"(jiminny/app)","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"•","depth":26,"role_description":"text"},{"role":"AXLink","text":"#11975 fix(security): npm dependency updates – 2026-04-16","depth":26,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"#11975 fix(security): npm dependency updates – 2026-04-16","depth":27,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"secfix/npm-20260416","depth":27,"role_description":"text"},{"role":"AXStaticText","text":")","depth":26,"role_description":"text"},{"role":"AXLink","text":"View workflow run","depth":26,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"View workflow run","depth":27,"role_description":"text"},{"role":"AXButton","text":"Ilian Kyuchukov","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":25,"role_description":"text"},{"role":"AXLink","text":"Yesterday at 9:31:41 AM","depth":25,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9:31 AM","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"Has there been any changes related to Hubspot?","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"The following Sentry has been escalating over the night with 14k events:","depth":25,"role_description":"text"},{"role":"AXLink","text":"https://jiminny.sentry.io/issues/7007366572/?project=82419&referrer=metric-issue-contributing-issues-issue-stream","depth":25,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"https://jiminny.sentry.io/issues/7007366572/?project=82419&referrer=metric-issue-contributing-issues-issue-stream","depth":26,"role_description":"text"},{"role":"AXLink","text":"SevenShores\\Hubspot\\Exceptions\\BadRequest","depth":28,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"SevenShores\\Hubspot\\Exceptions\\BadRequest","depth":29,"role_description":"text"},{"role":"AXStaticText","text":"/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php in Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest","depth":27,"role_description":"text"},{"role":"AXStaticText","text":"Client error: `POST","depth":29,"role_description":"text"},{"role":"AXLink","text":"https://api.hubapi.com/crm/v3/objects/contact/search","depth":29,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"` resulted in a `429 Too Many Requests` response:","depth":29,"role_description":"text"},{"role":"AXStaticText","text":"{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019db2b6-c (truncated...","depth":29,"role_description":"text"},{"role":"AXStaticText","text":"Events:","depth":27,"role_description":"text"},{"role":"AXStaticText","text":"14202","depth":27,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"role_description":"text"},{"role":"AXStaticText","text":"State:","depth":27,"role_description":"text"},{"role":"AXStaticText","text":"Ongoing","depth":27,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"role_description":"text"},{"role":"AXStaticText","text":"First Seen:","depth":27,"role_description":"text"},{"role":"AXStaticText","text":"2025-11-08","depth":27,"role_description":"text"},{"role":"AXButton","text":"Resolve","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Resolve","depth":29,"role_description":"text"},{"role":"AXButton","text":"Archive","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Archive","depth":29,"role_description":"text"},{"role":"AXComboBox","text":"Select Assignee...","depth":28,"role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"See more","depth":26,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Added by","depth":27,"role_description":"text"},{"role":"AXLink","text":"Sentry","depth":27,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Sentry","depth":28,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":27,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":27,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":27,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Nikolay Nikolov","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":25,"role_description":"text"},{"role":"AXLink","text":"Yesterday at 9:40:55 AM","depth":25,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9:40 AM","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"We have changed processing of webhooks from 30 min to 5 min smaller batches","depth":26,"role_description":"text"},{"role":"AXPopUpButton","text":"Jump to date","depth":24,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"New","depth":24,"role_description":"text"},{"role":"AXButton","text":"CircleCI","depth":25,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"APP","depth":25,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"role_description":"text"},{"role":"AXLink","text":"Today at 8:54:46 AM","depth":25,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8:54 AM","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"1 PR with vulnerability fixes is ready for review","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"Please take a look at the code and confirm that everything works properly on your local machine or on a planet environment","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"Pull requests","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"(jiminny/app)","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"•","depth":26,"role_description":"text"},{"role":"AXLink","text":"#11975 fix(security): npm dependency updates – 2026-04-16","depth":26,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"#11975 fix(security): npm dependency updates – 2026-04-16","depth":27,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":26,"role_description":"text"},{"role":"AXStaticText","text":"secfix/npm-20260416","depth":27,"role_description":"text"},{"role":"AXStaticText","text":")","depth":26,"role_description":"text"},{"role":"AXLink","text":"View workflow run","depth":26,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"View workflow run","depth":27,"role_description":"text"},{"role":"AXTextArea","text":"","depth":24,"value":"","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Conversation marked as unread from this message","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"Channel engineering","depth":11,"role_description":"text"}]...
|
1448138701810570852
|
2917124252003613454
|
click
|
hybrid
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
c-learning-people
confusion-clinic
curiosity_lab
deal-insights-dev
engineering
frontend
general
infra-changes
jiminny-bg
people-with-copilot-licences
people-with-zoom-phone-licences
platform-team
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Petko Kashinski
Stoyan Tomov
Aneliya Angelova
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Nikolay Yankov
Nikolay Nikolov
Mario Georgiev
Todor Stamatov
Gabriela Dureva
Vasil Vasilev
Galya Dimitrova
Stefka Stoyanova
Stoyan Tanev
Nikolay Ivanov
Jira Cloud
Toast
Messages
Messages
Canvas
Canvas
Files
Files
More
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
CircleCI
APP
Apr 21st at 8:51:40 AM
8:51 AM
1 PR with vulnerability fixes is ready for review
Please take a look at the code and confirm that everything works properly on your local machine or on a planet environment
Pull requests
(jiminny/app)
•
#11975 fix(security): npm dependency updates – 2026-04-16
#11975 fix(security): npm dependency updates – 2026-04-16
(
secfix/npm-20260416
)
View workflow run
View workflow run
6 replies
Last reply 1 day ago
View thread
Jump to date
CircleCI
APP
Yesterday at 8:51:39 AM
8:51 AM
1 PR with vulnerability fixes is ready for review
Please take a look at the code and confirm that everything works properly on your local machine or on a planet environment
Pull requests
(jiminny/app)
•
#11975 fix(security): npm dependency updates – 2026-04-16
#11975 fix(security): npm dependency updates – 2026-04-16
(
secfix/npm-20260416
)
View workflow run
View workflow run
Ilian Kyuchukov
Yesterday at 9:31:41 AM
9:31 AM
Has there been any changes related to Hubspot?
The following Sentry has been escalating over the night with 14k events:
https://jiminny.sentry.io/issues/7007366572/?project=82419&referrer=metric-issue-contributing-issues-issue-stream
https://jiminny.sentry.io/issues/7007366572/?project=82419&referrer=metric-issue-contributing-issues-issue-stream
SevenShores\Hubspot\Exceptions\BadRequest
SevenShores\Hubspot\Exceptions\BadRequest
/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php in Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::executeSearchRequest
Client error: `POST
https://api.hubapi.com/crm/v3/objects/contact/search
` resulted in a `429 Too Many Requests` response:
{"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT","correlationId":"019db2b6-c (truncated...
Events:
14202
State:
Ongoing
First Seen:
2025-11-08
Resolve
Resolve
Archive
Archive
Select Assignee...
See more
Added by
Sentry
Sentry
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Nikolay Nikolov
Yesterday at 9:40:55 AM
9:40 AM
We have changed processing of webhooks from 30 min to 5 min smaller batches
Jump to date
New
CircleCI
APP
Today at 8:54:46 AM
8:54 AM
1 PR with vulnerability fixes is ready for review
Please take a look at the code and confirm that everything works properly on your local machine or on a planet environment
Pull requests
(jiminny/app)
•
#11975 fix(security): npm dependency updates – 2026-04-16
#11975 fix(security): npm dependency updates – 2026-04-16
(
secfix/npm-20260416
)
View workflow run
View workflow run
Conversation marked as unread from this message
Channel engineering
iTerm2ShellEditViewSessionScriptsProfilesWindowHelpDaily - Platform • in 31 m100%8Thu 23 Apr 9:14:13ffmpeg181DOCKER• 881-zshО ₴2-zsh83* Build full day activi... #4ffmpeg885-zsh86APP (-zsh)87ec2-user@ip-10-..• *8warning:telemetry isenabled.onlyerror-leveltodisable, usethe --disable-telemetry flag.data will be sent.checklatestchanges here:https://github.com/screenpipe/screenpipe/releases2026-04-23T09:13:00.626925ZINFOscreenpipe:starting UIevent capture2026-04-23T09:13:00.644081ZINFOscreenpipe_engine::ui_recorder: Starting UIevent capture2026-04-23T09:13:00.659604ZINFOscreenpipe_engine::ui_recorder: UI recording session started: 64e213bd-3690-487f-90be-4e83480404162026-04-23T09:13:00.659568ZINFOscreenpipe_engine::calendar_speaker_id: speaker identification: started (user_name=<not set>)2026-04-23T09:13:00.659937ZINFOscreenpipe_engine::hot_frame_cache: hot_frame_cache: warming from DB (2026-04-22 06:13:00.659935 UTC to 2026-04-2306:13:00.659935 UTC)2026-04-23709:13:00.660681ZINFO2026-04-23T09:13:00.671031ZINFOscreenpipe_engine::meeting_detector: meeting V2: detection loop started (base_interval=5s, profiles=12)screenpipe_engine::server: Server listening on [IP_ADDRESS]:30302026-04-23T09:13:00.676357ZINFOscreenpipe_connect::mdns: mdns: advertising screenpipe on port 30302026-04-23109:13:02.8223462INFOscreenpipe_engine::vision_manager::manager: Starting vision recording2026-04-23T09:13:02.822390ZINFOscreenpipe_engine::vision_manager::manager: Starting event-driven capture for monitor 1 (device: monitor_1)2026-04-23T09:13:02.822429ZINFOscreenpipe_engine::event_driven_capture: event-driven capture started for monitor 1 (device: monitor_1)2026-04-23T09:13:03.612915ZINFOscreenpipe_engine::vision_manager::manager: Starting vision recording2026-04-23T09:13:03.612969ZINFOscreenpipe_engine::vision_manager::manager: Starting event-driven capture for monitor 2 (device: monitor_2)2026-04-23T09:13:03.612981ZINFOscreenpipe_engine::vision_manager::manager: VisionManager started with 2/2 monitor(s)2026-04-23709:13:03.612988ZINFO screenpipe_engine::vision_manager::monitor_watcher: Starting monitor watcher (event-driven via CGDisplayRegisterReconfigurationCallback, 60s backstop poll)2026-04-23T09:13:03.613032ZINFO2026-04-23T09:13:04.172201Zscreenpipe_engine::event_driven_capture:event-driven capture started for monitor 2 (device: monitor_2)WARN sqlx::query:summary="SELECT f.id, f.timestamp, f.offset_index,hinseviceindb.statement="\n\nSELECT\n f.id,\nf.timestamp, \nf.offset_index, \n COALESCE(\nSUBSTR(f.full_text,1, 200), \nSUBSTR(f.accessibility_text, 1, 200), \nERE\not.frame_id = f.id\nLIMIT\n1\n)\n ) as text, \n(\nSELECT\nSUBSTR(ot.text, 1, 200)\nFROM\nocr_text ot\nWHCOALESCE(\nf.app_name, \nSELECT\not\not.frame_id = f.id\nf.window_name, \not.app_name\nFROM\nocr_textWHERE\nLIMIT\n1\n)\n ) as app_name, \nSELECT\not.window_name\nFROM\nCOALESCEC\n(\nocr_text ot\nWHERE\not.frame_id = f.id\nLIMIT\n1\n)\n ) as window_name, \n COALESCE(vc.device_name, f.device_name) as screen_device, \nCOALESCE(vc.file_path, f.snapshot_path) as video_path, \n COALESCE(vc.fps, 0.033) as chunk_fps, \nf.browser_url,\n f.machine_id\nFROM\n frames f\nLEFT JOIN video_chunks vc ON f.video_chunk_id = vc.id\nWHERE\n f.timestamp >= ?1\n AND f.timestamp <= ?2\n AND COALESCE(vc.file_path, f.snapshot_path,'') NOT LIKE 'cloud://%' \nORDER BY\n f.timestampDESC, \nf.offset_index DESC\nLIMIT\n10000\n"rows_affected=0 rows_returned=4305 elapsed=3.511492208s2026-04-23T09:13:04.183200Z2026-04-23T09:13:07.349282ZINFO screenpipe_engine::hot_frame_cache: hot_frame_cache: warmed with 4304 frame entries, coverage from 2026-04-22 06:13:00.659935 UTCINFO2026-04-23T09:13:07.830685ZINFOsck_rs::stream_manager: persistent SCK stream started for display 1 (1440x900, 2fps, 3 excluded)screenpipe_engine::event_driven_capture: startup capture for monitor 1: frame_id=72776, dur=70ms2026-04-23T09:13:08.836958ZINFO sck_rs::stream_manager: persistent SCK stream started for display 2 (3008x1253, 2fps, 3 excluded)2026-04-23T09:13:09.378786ZINFOscreenpipe_engine::event_driven_capture: startup capture for monitor 2: frame_id=72777, dur=124ms2026-04-23T09:13:09.380863ZINFO sck_rs::stream_manager: invalidated persistent stream for display 22026-04-23T09:13:32.742255ZINFO sck_rs::stream_manager: persistent SCK stream started for display 2 (3008x1253, 2fps, 3 excluded)2026-04-23T09:14:01.764079ZINFO screenpipe_engine::event_driven_capture: content dedup: skipping capture for monitor 1 (hash=4702465002163205464, trigger=click)2026-04-23T09:14:04.223149ZINFO screenpipe_engine::event_driven_capture: content dedup: skipping capture for monitor 2 (hash=4702465002163205464, trigger=visual_change)2026-04-23T09:14:06.953796ZWARN sqlx::query:summary="SELECT id,snapshot_path, device_name, "db.statement="\n\nSELECT\nid, \nsnapshot_path, \n device_name, \n timestamp\nFROM\nframes \nWHERE\nsnapshot_path IS NOT NULL\nAND timestamp ‹ ?1\nORDER BYn device_name, In timestamp ASC\nLIMIT\n 5000\n" rows_affected-0 rows_returned-147 elapsed-6.3719465s2026-04-23709:14:06.954006Z2026-04-23T09:14:12.566935ZINFO screenpipe_engine::snapshot_compaction: snapshot compaction: found 147 eligible framesINFO screenpipe_engine::snapshot_compaction: snapshot compaction: 70 frames, 10.3MB → 2.7MB (3.8x), 70 JPEGS deleted...
|
NULL
|