|
58016
|
2042
|
0
|
2026-05-19T11:26:11.415208+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779189971415_m1.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskAnythingPromptServiceTest
Run 'AskAnythingPromptServiceTest'
Debug 'AskAnythingPromptServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
100
3
34
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Kiosk\AutomatedReports;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Carbon\Exceptions\InvalidFormatException;
use DateTime;
use DateTimeInterface;
use DateTimeZone;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\ActivitySearch\FilterDefinition\InputTypeEnum;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Partner;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Throwable;
class AutomatedReportsService
{
public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';
public const string TYPE_ASK_JIMINNY = 'ask_jiminny';
/**
* Standard report types (used by kiosk for existing automated reports).
*/
// @TODO this will add filter, however if we need to control feature by FF we need conditional logic
public const array TYPES = [
['id' => 'exec_summary', 'name' => 'Exec Summary'],
['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],
['id' => 'product_feedback', 'name' => 'Product Feedback'],
['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],
// ['id' => 'questions', 'name' => 'Questions'],
// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],
];
public const array ALL_TYPES = [
...self::TYPES,
['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],
];
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';
/**
* Frequencies for standard (non-Ask Jiminny) reports.
*/
public const array FREQUENCIES = [
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
/**
* Frequencies for Ask Jiminny reports.
*/
public const array ASK_JIMINNY_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
];
public const string MEDIA_TYPE_PDF = 'pdf';
public const string MEDIA_TYPE_PODCAST = 'podcast';
public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];
public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];
public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];
public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];
public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];
public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];
public const int SENT_REPORT_AT_HOURS = 5;
public const string PDF_KEY = 'pdf';
public const string AUDIO_KEY = 'audio';
private const array ALL_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
private const string S3_DIR = 'reports';
private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];
private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly GroupRepository $groupRepository,
private readonly UserRepository $userRepository,
private readonly StageRepository $stageRepository,
private readonly DealStagesService $dealStagesService,
private readonly RecipientsService $recipientsService,
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly Webhook $webhookService,
private readonly BusDispatcher $dispatcher,
private readonly ActivityTypeService $activityTypeService,
private readonly PlaybookCategoryRepository $playbookCategoryRepository,
private readonly AskAnythingPromptService $askAnythingPromptService,
private readonly SearchRepository $activitySearchRepository,
private readonly AskAnythingRepository $askAnythingRepository,
) {
}
public static function getTypes(): array
{
$types = self::TYPES;
return array_map(static function ($type) {
return $type['id'];
}, $types);
}
public static function getCallTypes(): array
{
return array_map(static function ($callType) {
return $callType['id'];
}, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);
}
public static function getFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::FREQUENCIES);
}
// front-facing structure
public function getReportEnabledFieldData(bool $value = false): array
{
return [
'id' => 'report_enabled',
'label' => '',
'inputType' => InputTypeEnum::TOGGLE,
'value' => $value,
];
}
// Organizations = Teams
public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array
{
$options = $this->getTeams(partner: $partner);
if ($shortVersion) {
return [
'id' => 'organization',
'label' => 'Organization',
'options' => $options,
];
}
return [
'id' => 'organization',
'label' => 'Organization',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $options,
'value' => $value,
'dependencies' => [
'teams',
'deal_stage_at_call',
'current_deal_stage',
'recipients',
ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,
],
'dependsOn' => [],
];
}
// Teams = Groups
public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array
{
if ($shortVersion) {
return [
'id' => 'teams',
'label' => 'Team',
'options' => $options,
];
}
return [
'id' => 'teams',
'label' => 'Team',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => false,
'placeholder' => 'Select',
'options' => $options,
'value' => $value, // value should be an array of objects {id, name}
'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],
'dependsOn' => [],
];
}
public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array
{
$types = [];
if ($team instanceof Team) {
if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
$types = self::TYPES;
}
if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {
$types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];
}
} else {
$types = self::TYPES;
}
if ($shortVersion) {
return [
'id' => 'report_type',
'label' => 'Report Type',
'options' => $types,
];
}
return [
'id' => 'report_type',
'label' => 'Report Type',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $types,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getFrequencyFieldData(?string $value = null): array
{
return [
'id' => 'frequency',
'label' => 'Frequency',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => self::FREQUENCIES,
'value' => $value,
'dependencies' => ['period'],
'dependsOn' => [],
];
}
public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array
{
return [
'id' => 'period',
'label' => 'Select one-off period',
'inputType' => InputTypeEnum::DATE_RANGE,
'required' => true,
'placeholder' => 'Select',
'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],
'queryParams' => [
'startDate' => 'start_date_period',
'endDate' => 'end_date_period',
],
'dependencies' => [],
'dependsOn' => ['frequency'],
];
}
public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array
{
return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);
}
public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);
}
public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);
}
public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'deal_value',
'label' => 'Deal Value',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_deal_value',
'max' => 'max_deal_value',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array
{
$value = [];
$conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;
$dialerOn && $value[] = self::CALL_TYPE_DIALER;
return [
'id' => 'call_type',
'label' => 'Call Type',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => [
self::CALL_TYPE_CONFERENCE,
self::CALL_TYPE_DIALER,
],
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getMediaTypeFieldData(?AutomatedReport $report = null): array
{
$value = [];
if ($report) {
$value = $this->transformMediaTypes($report);
}
return [
'id' => 'media_types',
'label' => 'Export as',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => self::MEDIA_TYPE_OBJECTS,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'call_duration',
'label' => 'Call Duration',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_call_duration',
'max' => 'max_call_duration',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getRecipientsFieldData(?Team $team = null, array $value = []): array
{
return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);
}
public function getJiminnyRecipientsFieldData(array $value = []): array
{
return $this->recipientsService->getJiminnyRecipientsFieldData($value);
}
public function getAdditionalPromptInputFieldData(?string $value = null): array
{
return [
'id' => 'additional_prompt_input',
'label' => 'Special requirements',
'inputType' => InputTypeEnum::TEXTAREA,
'required' => false,
'placeholder' => 'What should be the focus of the report?',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCustomReportNameFieldData(?string $value = null): array
{
return [
'id' => 'custom_name',
'label' => 'Custom report name',
'inputType' => InputTypeEnum::TEXT,
'required' => false,
'placeholder' => 'Enter custom name',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
// data providers
public function getTeams(?Partner $partner = null): array
{
$teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);
$teamData = [];
foreach ($teams as $team) {
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
continue;
}
$teamData[] = $this->transformTeam($team);
}
return $teamData;
}
public function getTeamGroups(string $teamUuid): array
{
$data = [];
$team = $this->getTeam($teamUuid);
if ($team !== null) {
$groups = $team->groups()->get();
foreach ($groups as $group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
];
}
}
return $data;
}
public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array
{
$data = [];
$teams = $this->getTeams(partner: $partner);
foreach ($teams as $team) {
if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {
continue;
}
$data[] = [
'label' => $team['name'],
'groups' => $this->getTeamGroups($team['id']),
];
}
return $data;
}
public function getTeam(string $teamUuid): ?Team
{
return $this->teamRepository->idOrUuid($teamUuid);
}
public function getTeamById(int $teamId): ?Team
{
return $this->teamRepository->find($teamId);
}
public function getGroupsUuids(AutomatedReport $report): array
{
$uuids = [];
$reportGroups = $report->getGroups();
foreach ($reportGroups as $groupId) {
if ($group = $this->groupRepository->find($groupId)) {
$uuids[] = $group->getUuid();
}
}
return $uuids;
}
public function getPlaybookCategoriesUuids(AutomatedReport $report): array
{
$uuids = [];
$playbookCategories = $report->getPlaybookCategories();
foreach ($playbookCategories as $id) {
if ($category = $this->playbookCategoryRepository->find($id)) {
$uuids[] = $category->getUuid();
}
}
return $uuids;
}
public function getDealAtCallStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getDealAtCallStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getCurrentDealStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getCurrentDealStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getRecipients());
}
public function getJiminnyUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function extractUserUuids(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => $user->getUuid())
->values()
->all();
}
// get mail data
public function getRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getRecipients());
}
/**
* @return array<UserContract>
*/
public function getRecipientUserObjects(AutomatedReport $report): array
{
$userIds = $report->getRecipients()['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->values()
->all();
}
private function getJiminnyRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function buildRecipientUsers(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => [
'email' => $user->getEmailAddress(),
'name' => $user->getName(),
'timezone' => $user->getTimezone()->getName(),
])
->values()
->all();
}
public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array
{
if ($report->isAskJiminnyReport()) {
$recipients = $this->resolveAskJiminnyRecipients($report);
} else {
$recipients = $this->getRecipientUsers($report);
if ($includeJiminny) {
$recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));
}
}
$emails = [];
return array_values(array_filter(
$recipients,
static function ($recipient) use (&$emails) {
if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {
return false;
}
$emails[] = $recipient['email'];
return true;
}
));
}
private function resolveAskJiminnyRecipients(AutomatedReport $report): array
{
$recipients = [];
$creator = $report->getCreator();
if ($creator !== null) {
$recipients[] = [
'email' => $creator->getEmailAddress(),
'name' => $creator->getName(),
'timezone' => $creator->getTimezone()->getName(),
];
}
return array_merge(
$recipients,
$this->buildRecipientUsers($report->getRecipients()),
$this->getGroupRecipientUsers($report),
);
}
private function getGroupRecipientUsers(AutomatedReport $report): array
{
$users = [];
foreach ($report->getGroups() as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group === null) {
continue;
}
foreach ($group->getMembers() as $member) {
$users[] = [
'email' => $member->getEmailAddress(),
'name' => $member->getName(),
'timezone' => $member->getTimezone()->getName(),
];
}
}
return $users;
}
public function getReportTypeName(AutomatedReportResult $report): string
{
$type = $report->getReport()->getType();
$getType = $this->transformReportType($type);
return $getType['name'];
}
public function getReportPeriodName(AutomatedReportResult $report): string
{
$from = $report->getFromDate();
$to = $report->getToDate();
$frequency = $report->getReport()->getFrequency();
if ($from === null || $to === null) {
if (! $report->getReport()->isAskJiminnyReport()) {
$invalidPeriod = $from === null ? 'from' : 'to';
throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);
}
$timezone = $report->getReport()->getCreator()?->getTimezone();
$period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);
$from = $period['fromDate'];
$to = $period['toDate'];
}
return $this->formatReportPeriodName($frequency, $from, $to);
}
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 getReportTeamsName(AutomatedReportResult $report): string
{
$groups = $report->getGroups();
if (empty($groups)) {
return 'All';
}
// Get group names from repository
$groupNames = [];
foreach ($groups as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group) {
$groupNames[] = $group->getName();
}
}
if (count($groupNames) === 1) {
// Single team format
$teamsName = $groupNames[0];
} else {
// Multiple teams format
$teamsName = implode(', ', $groupNames);
}
return $teamsName;
}
public function getReportFileName(AutomatedReportResult $report): string
{
$customName = $report->getReport()->getCustomName();
$periodName = $this->getReportPeriodName($report);
$filenameSuffix = $this->getFilenameSuffix($report);
if ($customName) {
if ($filenameSuffix) {
$customName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$customName} - {$periodName}");
}
$baseName = $this->getReportTypeName($report);
if ($filenameSuffix) {
$baseName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}");
}
public function getReportFileNameWithExtension(AutomatedReportResult $result): string
{
$extension = $this->getMediaTypeMetadata($result)['extension'];
return $this->getReportFileName($result) . '.' . $extension;
}
public function sanitizeFileName(string $fileName): string
{
return str_replace(['/', '\\'], '-', $fileName);
}
public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool
{
$recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);
if (in_array($user->getId(), $recipientIds, true)) {
return true;
}
if ($report->isAskJiminnyReport()) {
$groupId = $user->getGroupId();
if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {
return true;
}
}
return false;
}
public function transformReportResults(Collection $automatedReportResults): array
{
$data = [];
foreach ($automatedReportResults as $automatedReportResult) {
/** @var AutomatedReportResult $automatedReportResult */
$report = $automatedReportResult->getReport();
$createdBy = $report->getCreator();
$creator = [
'id' => $createdBy?->getUuid(),
'name' => $createdBy?->getName(),
'email' => $createdBy?->getEmailAddress(),
'photoUrl' => $createdBy?->getPhotoUrl(),
];
$data[] = [
'id' => $automatedReportResult->getUuid(),
'name' => $automatedReportResult->getName(),
'frequency' => $this->transformFrequency($report->getFrequency()),
'recipients' => $this->buildRecipients($report),
'report_type' => $this->transformReportType($report->getType()),
'media_type' => $automatedReportResult->getMediaType(),
'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),
'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),
'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),
'creator' => $creator,
];
}
return $data;
}
private function buildRecipients(AutomatedReport $report): array
{
$creatorUuid = $report->getCreator()?->getUuid();
$recipients = array_values(array_filter(
$this->transformRecipients($report->getRecipients()),
static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,
));
if (! $report->isAskJiminnyReport()) {
return $recipients;
}
return [
...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),
...$recipients,
];
}
public function hasCallTypeConference(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);
}
public function hasCallTypeDialer(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);
}
// transformers
private function transformTeam(Team $team): array
{
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
return [];
}
return [
'id' => $team->getUuid(),
'name' => $team->getName(),
];
}
private function transformReportFullView(AutomatedReport $report): array
{
$base = $this->transformReportBase($report);
return $report->getType() === self::TYPE_ASK_JIMINNY
? $base + $this->transformAskJiminnyFields($report)
: $base + $this->transformStandardReportFields($report);
}
private function transformReportBase(AutomatedReport $report): array
{
return [
'id' => $report->getUuid(),
'organization' => $this->transformOrganization(team: $report->getTeam()),
'report_type' => $this->transformReportType($report->getType()),
'frequency' => $this->transformFrequency($report->getFrequency()),
];
}
private function transformStandardReportFields(AutomatedReport $report): array
{
$team = $report->getTeam();
return [
'report_enabled' => $report->getStatus(),
'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),
'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),
'deal_value_min' => $report->getDealValueMin(),
'deal_value_max' => $report->getDealValueMax(),
'call_types' => $this->transformCallType($report->getCallTypes()),
'media_types' => $this->transformMediaTypes($report),
'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),
'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),
'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),
'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),
'recipients' => $this->transformRecipients($report->getRecipients()),
'created_by' => $this->transformCreator($report->getCreator()),
'additional_prompt_input' => $report->getAdditionalPromptInput(),
'custom_name' => $report->getCustomName(),
'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),
'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),
'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),
];
}
private function transformAskJiminnyFields(AutomatedReport $report): array
{
$team = $report->getTeam();
$creatorId = $report->getAttribute('created_by');
$explicitUserIds = array_values(array_filter(
$report->getRecipients()['users'] ?? [],
static fn ($id) => $id !== $creatorId
));
return [
'report_name' => $report->getCustomName(),
'enabled' => $report->getStatus(),
'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),
'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),
'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),
'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),
];
}
private function transformOrganization(?Team $team): array
{
return [
'id' => $team?->getUuid(),
'name' => $team?->getName(),
];
}
private function transformReportType(string $type): array
{
foreach (self::ALL_TYPES as $typeItem) {
if ($typeItem['id'] === $type) {
return $typeItem;
}
}
return [];
}
private function transformCallType(array $types): array
{
$result = [];
$callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];
foreach ($types as $type) {
foreach ($callTypes as $callTypeItem) {
if ($callTypeItem['id'] === $type) {
$result[] = $callTypeItem;
break;
}
}
}
return $result;
}
private function transformMediaTypes(AutomatedReport $report): array
{
$values = [];
foreach ($report->getMediaTypes() as $mediaType) {
if (! in_array($mediaType, self::MEDIA_TYPES, true)) {
continue;
}
$values[] = match ($mediaType) {
self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,
self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,
};
}
return $values;
}
private function transformFrequency(string $frequency): array
{
foreach (self::ALL_FREQUENCIES as $frequencyItem) {
if ($frequencyItem['id'] === $frequency) {
return $frequencyItem;
}
}
return [];
}
public function transformDurationToMinutes(?int $duration): ?int
{
if (! $duration) {
return null;
}
return (int) ($duration / 60);
}
private function transformGroups(?Team $team, array $groupsIds): array
{
if (empty($groupsIds) || ! $team) {
return [];
}
$data = [];
foreach ($groupsIds as $groupId) {
$group = $team->groups()->where('id', $groupId)->first();
if ($group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
'photoUrl' => $group->getPhotoUrl(),
];
}
}
return $data;
}
private function transformStages(?Team $team, array $stagesIds): array
{
if (empty($stagesIds) || ! $team) {
return [];
}
$data = [];
foreach ($stagesIds as $stageId) {
$stage = $team->stages()->where('id', $stageId)->first();
if ($stage) {
$data[] = [
'id' => $stage->getUuid(),
'name' => $stage->getName(),
];
}
}
return $data;
}
private function transformRecipients(array $recipients): array
{
$users = [];
foreach ($recipients['users'] ?? [] as $userId) {
$users[] = $this->transformUser($userId);
}
return $users;
}
private function transformCreator(?User $user): ?array
{
if ($user === null) {
return null;
}
return $this->transformUser($user->getId());
}
private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array
{
if ($prompt === null) {
return null;
}
return [
'id' => $prompt->getUuid(),
'name' => $prompt->getTitle(),
];
}
private function transformSafeSearch(?Search $search): ?array
{
if ($search === null) {
return null;
}
return [
'id' => $search->getUuid(),
'name' => $search->getName(),
];
}
private function transformUser(int $userId): array
{
/* @var ?User $user */
$user = $this->userRepository->find($userId);
return [
'id' => $user?->getUuid(),
'name' => $user?->getName(),
'email' => $user?->getEmailAddress(),
'photoUrl' => $user?->getPhotoUrl(),
];
}
public function create(array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$validatedData['created_by'] = auth()->id();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
public function update(string $uuid, array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$report = $this->automatedReportsRepository->findByUuid($uuid);
if (! $report) {
throw new InvalidArgumentException('Report not found');
}
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
/**
* Create an Ask Jiminny report.
*/
public function createAskJiminnyReport(array $data, User $creator): array
{
$validatedData = $this->validateAskJiminnyReportData($data, $creator);
$validatedData['created_by'] = $creator->getId();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
return $this->transformReportFullView($automatedReport);
}
/**
* Update an Ask Jiminny report.
*/
public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array
{
if (! $report->isAskJiminnyReport()) {
throw new InvalidArgumentException('Report is not an Ask Jiminny report');
}
$validatedData = $this->validateAskJiminnyReportData($data, $user);
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
return $this->transformReportFullView($automatedReport);
}
public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array
{
if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {
throw new InvalidArgumentException(
'This report is missing a saved search or prompt. ' .
'Edit the report to complete the setup before enabling it.'
);
}
$this->automatedReportsRepository->update($report, ['status' => $status]);
return $this->transformReportFullView($report->fresh());
}
/**
* Validate and transform data for Ask Jiminny reports.
*/
private function validateAskJiminnyReportData(array $data, User $user): array
{
// Validate name
$name = trim($data['report_name'] ?? '');
if (empty($name)) {
throw new InvalidArgumentException('Report name is required');
}
if (mb_strlen($name) > 50) {
throw new InvalidArgumentException('Report name must be 50 characters or less');
}
// Validate frequency (only daily, weekly, monthly for Ask Jiminny)
$frequency = $data['frequency'] ?? null;
$askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];
if (! in_array($frequency, $askJiminnyFrequencies, true)) {
throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');
}
// Validate expiration date
$expiresAt = $data['expires_on'] ?? null;
if (empty($expiresAt)) {
throw new InvalidArgumentException('Expiration date is required');
}
try {
$expiresAtDate = Carbon::parse($expiresAt);
} catch (InvalidFormatException $e) {
throw new InvalidArgumentException('Expiration date format is invalid');
}
$maxExpiration = Carbon::now()->addYear()->endOfDay();
if ($expiresAtDate->gt($maxExpiration)) {
throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');
}
if ($expiresAtDate->isPast()) {
throw new InvalidArgumentException('Expiration date cannot be in the past');
}
// Validate saved search
$activitySearchId = $data['saved_search'] ?? null;
if (empty($activitySearchId)) {
throw new InvalidArgumentException('Saved search is required');
}
$savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);
if (! $savedSearch) {
throw new InvalidArgumentException('Saved search not found or does not belong to you');
}
// Validate saved prompt
$askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;
if (empty($askAnythingPromptId)) {
throw new InvalidArgumentException('Ask Jiminny prompt is required');
}
$prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);
if (! $prompt) {
throw new InvalidArgumentException('Ask Jiminny prompt not found');
}
// Validate status
$status = $data['enabled'] ?? false;
$recipientUserIds = [$user->getId()];
if (! empty($data['share_users'])) {
$sharedUserIds = $this->validateAndGetUserIdsByTeam(
$user->team,
(array) $data['share_users']
);
$recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);
}
$sharedGroupIds = [];
if (! empty($data['share_teams'])) {
$sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);
}
$recipientUserIds = array_values(array_unique($recipientUserIds));
return [
'team_id' => $user->getTeamId(),
'type' => self::TYPE_ASK_JIMINNY,
'status' => (bool) $status,
'frequency' => $frequency,
'custom_name' => $name,
'activity_search_id' => $savedSearch->getId(),
'ask_anything_prompt_id' => $prompt->getId(),
'expires_at' => $expiresAtDate->toDateString(),
'media_types' => [self::MEDIA_TYPE_PDF],
'call_types' => [],
'recipients' => ['users' => $recipientUserIds],
'groups' => $sharedGroupIds,
];
}
public static function getAskJiminnyFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::ASK_JIMINNY_FREQUENCIES);
}
public function getAskJiminnyReportFilters(User $user): array
{
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
return [
[
'id' => 'prompt',
'label' => 'Prompt',
'options' => $prompts,
],
[
'id' => 'saved_search',
'label' => 'Saved Search',
'options' => $savedSearches,
],
];
}
public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array
{
$team = $user->getTeam();
$userTimezone = $user->getTimezone();
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
$teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [
'id' => $group->getUuid(),
'name' => $group->getName(),
])->values()->all();
$shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];
$sharedTeamsValue = [];
$sharedUsersValue = [];
if ($report) {
$sharedTeamsValue = $this->transformGroups($team, $report->getGroups());
$recipientUserIds = $report->getRecipients()['users'] ?? [];
$creatorId = $report->getAttribute('created_by');
$sharedUserIds = array_values(array_filter(
$recipientUserIds,
static fn ($id) => $id !== $creatorId
));
$sharedUsersValue = collect($sharedUserIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (User $u) => [
'id' => $u->getUuid(),
'name' => $u->getName(),
])
->values()
->all();
}
return [
'fields' => [
[
'id' => 'enabled',
'inputType' => InputTypeEnum::TOGGLE,
'label' => '',
'value' => $report?->getStatus() ?? false,
],
[
'id' => 'report_name',
'inputType' => InputTypeEnum::TEXT,
'label' => 'Name',
'placeholder' => 'Enter name',
'required' => true,
'validation' => ['maxLength' => 50],
'value' => $report?->getCustomName() ?? '',
...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects<br/>Some incoming commits are not fetched<br/>","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,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskAnythingPromptServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskAnythingPromptServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskAnythingPromptServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"100","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"34","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"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\\Services\\Kiosk\\AutomatedReports;\n\nuse Carbon\\CarbonImmutable;\nuse Carbon\\CarbonInterface;\nuse Carbon\\Exceptions\\InvalidFormatException;\nuse DateTime;\nuse DateTimeInterface;\nuse DateTimeZone;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\InputTypeEnum;\nuse Jiminny\\Component\\AskAnything\\AskAnythingPromptService;\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Contracts\\Repositories\\PlaybookCategoryRepository;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\ModelNotFoundException;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Partner;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Repositories\\GroupRepository;\nuse Jiminny\\Repositories\\SearchRepository;\nuse Jiminny\\Repositories\\StageRepository;\nuse Throwable;\n\nclass AutomatedReportsService\n{\n public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';\n public const string TYPE_ASK_JIMINNY = 'ask_jiminny';\n\n /**\n * Standard report types (used by kiosk for existing automated reports).\n */\n // @TODO this will add filter, however if we need to control feature by FF we need conditional logic\n public const array TYPES = [\n ['id' => 'exec_summary', 'name' => 'Exec Summary'],\n ['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],\n ['id' => 'product_feedback', 'name' => 'Product Feedback'],\n ['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],\n// ['id' => 'questions', 'name' => 'Questions'],\n// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],\n ];\n\n public const array ALL_TYPES = [\n ...self::TYPES,\n ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],\n ];\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\n /**\n * Frequencies for standard (non-Ask Jiminny) reports.\n */\n public const array FREQUENCIES = [\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n\n /**\n * Frequencies for Ask Jiminny reports.\n */\n public const array ASK_JIMINNY_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ];\n\n public const string MEDIA_TYPE_PDF = 'pdf';\n public const string MEDIA_TYPE_PODCAST = 'podcast';\n public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];\n public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];\n public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];\n public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];\n\n public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];\n public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];\n public const int SENT_REPORT_AT_HOURS = 5;\n public const string PDF_KEY = 'pdf';\n public const string AUDIO_KEY = 'audio';\n\n private const array ALL_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n private const string S3_DIR = 'reports';\n private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];\n private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];\n\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly GroupRepository $groupRepository,\n private readonly UserRepository $userRepository,\n private readonly StageRepository $stageRepository,\n private readonly DealStagesService $dealStagesService,\n private readonly RecipientsService $recipientsService,\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly Webhook $webhookService,\n private readonly BusDispatcher $dispatcher,\n private readonly ActivityTypeService $activityTypeService,\n private readonly PlaybookCategoryRepository $playbookCategoryRepository,\n private readonly AskAnythingPromptService $askAnythingPromptService,\n private readonly SearchRepository $activitySearchRepository,\n private readonly AskAnythingRepository $askAnythingRepository,\n ) {\n }\n\n public static function getTypes(): array\n {\n $types = self::TYPES;\n\n return array_map(static function ($type) {\n return $type['id'];\n }, $types);\n }\n\n public static function getCallTypes(): array\n {\n return array_map(static function ($callType) {\n return $callType['id'];\n }, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);\n }\n\n public static function getFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::FREQUENCIES);\n }\n\n // front-facing structure\n public function getReportEnabledFieldData(bool $value = false): array\n {\n return [\n 'id' => 'report_enabled',\n 'label' => '',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'value' => $value,\n ];\n }\n\n // Organizations = Teams\n public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array\n {\n $options = $this->getTeams(partner: $partner);\n\n if ($shortVersion) {\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value,\n 'dependencies' => [\n 'teams',\n 'deal_stage_at_call',\n 'current_deal_stage',\n 'recipients',\n ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,\n ],\n 'dependsOn' => [],\n ];\n }\n\n // Teams = Groups\n public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array\n {\n if ($shortVersion) {\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value, // value should be an array of objects {id, name}\n 'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],\n 'dependsOn' => [],\n ];\n }\n\n public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array\n {\n $types = [];\n if ($team instanceof Team) {\n if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n $types = self::TYPES;\n }\n if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {\n $types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];\n }\n } else {\n $types = self::TYPES;\n }\n\n if ($shortVersion) {\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'options' => $types,\n ];\n }\n\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $types,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getFrequencyFieldData(?string $value = null): array\n {\n return [\n 'id' => 'frequency',\n 'label' => 'Frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::FREQUENCIES,\n 'value' => $value,\n 'dependencies' => ['period'],\n 'dependsOn' => [],\n ];\n }\n\n public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array\n {\n return [\n 'id' => 'period',\n 'label' => 'Select one-off period',\n 'inputType' => InputTypeEnum::DATE_RANGE,\n 'required' => true,\n 'placeholder' => 'Select',\n 'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],\n 'queryParams' => [\n 'startDate' => 'start_date_period',\n 'endDate' => 'end_date_period',\n ],\n 'dependencies' => [],\n 'dependsOn' => ['frequency'],\n ];\n }\n\n public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array\n {\n return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);\n }\n\n public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);\n }\n\n public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);\n }\n\n public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'deal_value',\n 'label' => 'Deal Value',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_deal_value',\n 'max' => 'max_deal_value',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array\n {\n $value = [];\n $conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;\n $dialerOn && $value[] = self::CALL_TYPE_DIALER;\n\n return [\n 'id' => 'call_type',\n 'label' => 'Call Type',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => [\n self::CALL_TYPE_CONFERENCE,\n self::CALL_TYPE_DIALER,\n ],\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getMediaTypeFieldData(?AutomatedReport $report = null): array\n {\n $value = [];\n\n if ($report) {\n $value = $this->transformMediaTypes($report);\n }\n\n return [\n 'id' => 'media_types',\n 'label' => 'Export as',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => self::MEDIA_TYPE_OBJECTS,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'call_duration',\n 'label' => 'Call Duration',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_call_duration',\n 'max' => 'max_call_duration',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getRecipientsFieldData(?Team $team = null, array $value = []): array\n {\n return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);\n }\n\n public function getJiminnyRecipientsFieldData(array $value = []): array\n {\n return $this->recipientsService->getJiminnyRecipientsFieldData($value);\n }\n\n public function getAdditionalPromptInputFieldData(?string $value = null): array\n {\n return [\n 'id' => 'additional_prompt_input',\n 'label' => 'Special requirements',\n 'inputType' => InputTypeEnum::TEXTAREA,\n 'required' => false,\n 'placeholder' => 'What should be the focus of the report?',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCustomReportNameFieldData(?string $value = null): array\n {\n return [\n 'id' => 'custom_name',\n 'label' => 'Custom report name',\n 'inputType' => InputTypeEnum::TEXT,\n 'required' => false,\n 'placeholder' => 'Enter custom name',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n // data providers\n public function getTeams(?Partner $partner = null): array\n {\n $teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);\n\n $teamData = [];\n foreach ($teams as $team) {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n continue;\n }\n\n $teamData[] = $this->transformTeam($team);\n }\n\n return $teamData;\n }\n\n public function getTeamGroups(string $teamUuid): array\n {\n $data = [];\n $team = $this->getTeam($teamUuid);\n\n if ($team !== null) {\n $groups = $team->groups()->get();\n\n foreach ($groups as $group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array\n {\n $data = [];\n $teams = $this->getTeams(partner: $partner);\n\n foreach ($teams as $team) {\n if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {\n continue;\n }\n\n $data[] = [\n 'label' => $team['name'],\n 'groups' => $this->getTeamGroups($team['id']),\n ];\n }\n\n return $data;\n }\n\n public function getTeam(string $teamUuid): ?Team\n {\n return $this->teamRepository->idOrUuid($teamUuid);\n }\n\n public function getTeamById(int $teamId): ?Team\n {\n return $this->teamRepository->find($teamId);\n }\n\n public function getGroupsUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportGroups = $report->getGroups();\n foreach ($reportGroups as $groupId) {\n if ($group = $this->groupRepository->find($groupId)) {\n $uuids[] = $group->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getPlaybookCategoriesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $playbookCategories = $report->getPlaybookCategories();\n foreach ($playbookCategories as $id) {\n if ($category = $this->playbookCategoryRepository->find($id)) {\n $uuids[] = $category->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getDealAtCallStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getDealAtCallStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getCurrentDealStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getCurrentDealStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getRecipients());\n }\n\n public function getJiminnyUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function extractUserUuids(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => $user->getUuid())\n ->values()\n ->all();\n }\n\n // get mail data\n public function getRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getRecipients());\n }\n\n /**\n * @return array<UserContract>\n */\n public function getRecipientUserObjects(AutomatedReport $report): array\n {\n $userIds = $report->getRecipients()['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->values()\n ->all();\n }\n\n private function getJiminnyRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function buildRecipientUsers(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => [\n 'email' => $user->getEmailAddress(),\n 'name' => $user->getName(),\n 'timezone' => $user->getTimezone()->getName(),\n ])\n ->values()\n ->all();\n }\n\n public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array\n {\n if ($report->isAskJiminnyReport()) {\n $recipients = $this->resolveAskJiminnyRecipients($report);\n } else {\n $recipients = $this->getRecipientUsers($report);\n if ($includeJiminny) {\n $recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));\n }\n }\n\n $emails = [];\n\n return array_values(array_filter(\n $recipients,\n static function ($recipient) use (&$emails) {\n if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {\n return false;\n }\n $emails[] = $recipient['email'];\n\n return true;\n }\n ));\n }\n\n private function resolveAskJiminnyRecipients(AutomatedReport $report): array\n {\n $recipients = [];\n\n $creator = $report->getCreator();\n if ($creator !== null) {\n $recipients[] = [\n 'email' => $creator->getEmailAddress(),\n 'name' => $creator->getName(),\n 'timezone' => $creator->getTimezone()->getName(),\n ];\n }\n\n return array_merge(\n $recipients,\n $this->buildRecipientUsers($report->getRecipients()),\n $this->getGroupRecipientUsers($report),\n );\n }\n\n private function getGroupRecipientUsers(AutomatedReport $report): array\n {\n $users = [];\n foreach ($report->getGroups() as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group === null) {\n continue;\n }\n foreach ($group->getMembers() as $member) {\n $users[] = [\n 'email' => $member->getEmailAddress(),\n 'name' => $member->getName(),\n 'timezone' => $member->getTimezone()->getName(),\n ];\n }\n }\n\n return $users;\n }\n\n public function getReportTypeName(AutomatedReportResult $report): string\n {\n $type = $report->getReport()->getType();\n\n $getType = $this->transformReportType($type);\n\n return $getType['name'];\n }\n\n public function getReportPeriodName(AutomatedReportResult $report): string\n {\n $from = $report->getFromDate();\n $to = $report->getToDate();\n $frequency = $report->getReport()->getFrequency();\n\n if ($from === null || $to === null) {\n if (! $report->getReport()->isAskJiminnyReport()) {\n $invalidPeriod = $from === null ? 'from' : 'to';\n\n throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);\n }\n\n $timezone = $report->getReport()->getCreator()?->getTimezone();\n $period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);\n $from = $period['fromDate'];\n $to = $period['toDate'];\n }\n\n return $this->formatReportPeriodName($frequency, $from, $to);\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 getReportTeamsName(AutomatedReportResult $report): string\n {\n $groups = $report->getGroups();\n\n if (empty($groups)) {\n return 'All';\n }\n\n // Get group names from repository\n $groupNames = [];\n foreach ($groups as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group) {\n $groupNames[] = $group->getName();\n }\n }\n\n if (count($groupNames) === 1) {\n // Single team format\n $teamsName = $groupNames[0];\n } else {\n // Multiple teams format\n $teamsName = implode(', ', $groupNames);\n }\n\n return $teamsName;\n }\n\n public function getReportFileName(AutomatedReportResult $report): string\n {\n $customName = $report->getReport()->getCustomName();\n $periodName = $this->getReportPeriodName($report);\n $filenameSuffix = $this->getFilenameSuffix($report);\n\n if ($customName) {\n if ($filenameSuffix) {\n $customName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$customName} - {$periodName}\");\n }\n\n $baseName = $this->getReportTypeName($report);\n\n if ($filenameSuffix) {\n $baseName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}\");\n }\n\n public function getReportFileNameWithExtension(AutomatedReportResult $result): string\n {\n $extension = $this->getMediaTypeMetadata($result)['extension'];\n\n return $this->getReportFileName($result) . '.' . $extension;\n }\n\n public function sanitizeFileName(string $fileName): string\n {\n return str_replace(['/', '\\\\'], '-', $fileName);\n }\n\n public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool\n {\n $recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);\n if (in_array($user->getId(), $recipientIds, true)) {\n return true;\n }\n\n if ($report->isAskJiminnyReport()) {\n $groupId = $user->getGroupId();\n if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {\n return true;\n }\n }\n\n return false;\n }\n\n public function transformReportResults(Collection $automatedReportResults): array\n {\n $data = [];\n foreach ($automatedReportResults as $automatedReportResult) {\n /** @var AutomatedReportResult $automatedReportResult */\n\n $report = $automatedReportResult->getReport();\n\n $createdBy = $report->getCreator();\n $creator = [\n 'id' => $createdBy?->getUuid(),\n 'name' => $createdBy?->getName(),\n 'email' => $createdBy?->getEmailAddress(),\n 'photoUrl' => $createdBy?->getPhotoUrl(),\n ];\n\n $data[] = [\n 'id' => $automatedReportResult->getUuid(),\n 'name' => $automatedReportResult->getName(),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n 'recipients' => $this->buildRecipients($report),\n 'report_type' => $this->transformReportType($report->getType()),\n 'media_type' => $automatedReportResult->getMediaType(),\n 'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),\n 'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),\n 'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),\n 'creator' => $creator,\n ];\n }\n\n return $data;\n }\n\n private function buildRecipients(AutomatedReport $report): array\n {\n $creatorUuid = $report->getCreator()?->getUuid();\n\n $recipients = array_values(array_filter(\n $this->transformRecipients($report->getRecipients()),\n static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,\n ));\n\n if (! $report->isAskJiminnyReport()) {\n return $recipients;\n }\n\n return [\n ...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),\n ...$recipients,\n ];\n }\n\n public function hasCallTypeConference(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);\n }\n\n public function hasCallTypeDialer(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);\n }\n\n // transformers\n private function transformTeam(Team $team): array\n {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n return [];\n }\n\n return [\n 'id' => $team->getUuid(),\n 'name' => $team->getName(),\n ];\n }\n\n private function transformReportFullView(AutomatedReport $report): array\n {\n $base = $this->transformReportBase($report);\n\n return $report->getType() === self::TYPE_ASK_JIMINNY\n ? $base + $this->transformAskJiminnyFields($report)\n : $base + $this->transformStandardReportFields($report);\n }\n\n private function transformReportBase(AutomatedReport $report): array\n {\n return [\n 'id' => $report->getUuid(),\n 'organization' => $this->transformOrganization(team: $report->getTeam()),\n 'report_type' => $this->transformReportType($report->getType()),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n ];\n }\n\n private function transformStandardReportFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n\n return [\n 'report_enabled' => $report->getStatus(),\n 'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),\n 'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),\n 'deal_value_min' => $report->getDealValueMin(),\n 'deal_value_max' => $report->getDealValueMax(),\n 'call_types' => $this->transformCallType($report->getCallTypes()),\n 'media_types' => $this->transformMediaTypes($report),\n 'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),\n 'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),\n 'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),\n 'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),\n 'recipients' => $this->transformRecipients($report->getRecipients()),\n 'created_by' => $this->transformCreator($report->getCreator()),\n 'additional_prompt_input' => $report->getAdditionalPromptInput(),\n 'custom_name' => $report->getCustomName(),\n 'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),\n 'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),\n 'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),\n ];\n }\n\n private function transformAskJiminnyFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n $creatorId = $report->getAttribute('created_by');\n $explicitUserIds = array_values(array_filter(\n $report->getRecipients()['users'] ?? [],\n static fn ($id) => $id !== $creatorId\n ));\n\n return [\n 'report_name' => $report->getCustomName(),\n 'enabled' => $report->getStatus(),\n 'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),\n 'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),\n 'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),\n 'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),\n ];\n }\n\n private function transformOrganization(?Team $team): array\n {\n return [\n 'id' => $team?->getUuid(),\n 'name' => $team?->getName(),\n ];\n }\n\n private function transformReportType(string $type): array\n {\n foreach (self::ALL_TYPES as $typeItem) {\n if ($typeItem['id'] === $type) {\n return $typeItem;\n }\n }\n\n return [];\n }\n\n private function transformCallType(array $types): array\n {\n $result = [];\n $callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];\n\n foreach ($types as $type) {\n foreach ($callTypes as $callTypeItem) {\n if ($callTypeItem['id'] === $type) {\n $result[] = $callTypeItem;\n\n break;\n }\n }\n }\n\n return $result;\n }\n\n private function transformMediaTypes(AutomatedReport $report): array\n {\n $values = [];\n\n foreach ($report->getMediaTypes() as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n continue;\n }\n\n $values[] = match ($mediaType) {\n self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,\n self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,\n };\n }\n\n return $values;\n }\n\n private function transformFrequency(string $frequency): array\n {\n foreach (self::ALL_FREQUENCIES as $frequencyItem) {\n if ($frequencyItem['id'] === $frequency) {\n return $frequencyItem;\n }\n }\n\n return [];\n }\n\n public function transformDurationToMinutes(?int $duration): ?int\n {\n if (! $duration) {\n return null;\n }\n\n return (int) ($duration / 60);\n }\n\n private function transformGroups(?Team $team, array $groupsIds): array\n {\n if (empty($groupsIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($groupsIds as $groupId) {\n $group = $team->groups()->where('id', $groupId)->first();\n\n if ($group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n 'photoUrl' => $group->getPhotoUrl(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformStages(?Team $team, array $stagesIds): array\n {\n if (empty($stagesIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($stagesIds as $stageId) {\n $stage = $team->stages()->where('id', $stageId)->first();\n\n if ($stage) {\n $data[] = [\n 'id' => $stage->getUuid(),\n 'name' => $stage->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformRecipients(array $recipients): array\n {\n $users = [];\n foreach ($recipients['users'] ?? [] as $userId) {\n $users[] = $this->transformUser($userId);\n }\n\n return $users;\n }\n\n private function transformCreator(?User $user): ?array\n {\n if ($user === null) {\n return null;\n }\n\n return $this->transformUser($user->getId());\n }\n\n private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array\n {\n if ($prompt === null) {\n return null;\n }\n\n return [\n 'id' => $prompt->getUuid(),\n 'name' => $prompt->getTitle(),\n ];\n }\n\n private function transformSafeSearch(?Search $search): ?array\n {\n if ($search === null) {\n return null;\n }\n\n return [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ];\n }\n\n private function transformUser(int $userId): array\n {\n /* @var ?User $user */\n $user = $this->userRepository->find($userId);\n\n return [\n 'id' => $user?->getUuid(),\n 'name' => $user?->getName(),\n 'email' => $user?->getEmailAddress(),\n 'photoUrl' => $user?->getPhotoUrl(),\n ];\n }\n\n public function create(array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $validatedData['created_by'] = auth()->id();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function update(string $uuid, array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $report = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $report) {\n throw new InvalidArgumentException('Report not found');\n }\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Create an Ask Jiminny report.\n */\n public function createAskJiminnyReport(array $data, User $creator): array\n {\n $validatedData = $this->validateAskJiminnyReportData($data, $creator);\n $validatedData['created_by'] = $creator->getId();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Update an Ask Jiminny report.\n */\n public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array\n {\n if (! $report->isAskJiminnyReport()) {\n throw new InvalidArgumentException('Report is not an Ask Jiminny report');\n }\n\n $validatedData = $this->validateAskJiminnyReportData($data, $user);\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array\n {\n if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {\n throw new InvalidArgumentException(\n 'This report is missing a saved search or prompt. ' .\n 'Edit the report to complete the setup before enabling it.'\n );\n }\n\n $this->automatedReportsRepository->update($report, ['status' => $status]);\n\n return $this->transformReportFullView($report->fresh());\n }\n\n /**\n * Validate and transform data for Ask Jiminny reports.\n */\n private function validateAskJiminnyReportData(array $data, User $user): array\n {\n // Validate name\n $name = trim($data['report_name'] ?? '');\n if (empty($name)) {\n throw new InvalidArgumentException('Report name is required');\n }\n if (mb_strlen($name) > 50) {\n throw new InvalidArgumentException('Report name must be 50 characters or less');\n }\n\n // Validate frequency (only daily, weekly, monthly for Ask Jiminny)\n $frequency = $data['frequency'] ?? null;\n $askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];\n if (! in_array($frequency, $askJiminnyFrequencies, true)) {\n throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');\n }\n\n // Validate expiration date\n $expiresAt = $data['expires_on'] ?? null;\n if (empty($expiresAt)) {\n throw new InvalidArgumentException('Expiration date is required');\n }\n\n try {\n $expiresAtDate = Carbon::parse($expiresAt);\n } catch (InvalidFormatException $e) {\n throw new InvalidArgumentException('Expiration date format is invalid');\n }\n $maxExpiration = Carbon::now()->addYear()->endOfDay();\n if ($expiresAtDate->gt($maxExpiration)) {\n throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');\n }\n if ($expiresAtDate->isPast()) {\n throw new InvalidArgumentException('Expiration date cannot be in the past');\n }\n\n // Validate saved search\n $activitySearchId = $data['saved_search'] ?? null;\n if (empty($activitySearchId)) {\n throw new InvalidArgumentException('Saved search is required');\n }\n $savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);\n if (! $savedSearch) {\n throw new InvalidArgumentException('Saved search not found or does not belong to you');\n }\n\n // Validate saved prompt\n $askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;\n if (empty($askAnythingPromptId)) {\n throw new InvalidArgumentException('Ask Jiminny prompt is required');\n }\n $prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);\n if (! $prompt) {\n throw new InvalidArgumentException('Ask Jiminny prompt not found');\n }\n\n // Validate status\n $status = $data['enabled'] ?? false;\n\n $recipientUserIds = [$user->getId()];\n\n if (! empty($data['share_users'])) {\n $sharedUserIds = $this->validateAndGetUserIdsByTeam(\n $user->team,\n (array) $data['share_users']\n );\n $recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);\n }\n\n $sharedGroupIds = [];\n if (! empty($data['share_teams'])) {\n $sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);\n }\n\n $recipientUserIds = array_values(array_unique($recipientUserIds));\n\n return [\n 'team_id' => $user->getTeamId(),\n 'type' => self::TYPE_ASK_JIMINNY,\n 'status' => (bool) $status,\n 'frequency' => $frequency,\n 'custom_name' => $name,\n 'activity_search_id' => $savedSearch->getId(),\n 'ask_anything_prompt_id' => $prompt->getId(),\n 'expires_at' => $expiresAtDate->toDateString(),\n 'media_types' => [self::MEDIA_TYPE_PDF],\n 'call_types' => [],\n 'recipients' => ['users' => $recipientUserIds],\n 'groups' => $sharedGroupIds,\n ];\n }\n\n public static function getAskJiminnyFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::ASK_JIMINNY_FREQUENCIES);\n }\n\n public function getAskJiminnyReportFilters(User $user): array\n {\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n return [\n [\n 'id' => 'prompt',\n 'label' => 'Prompt',\n 'options' => $prompts,\n ],\n [\n 'id' => 'saved_search',\n 'label' => 'Saved Search',\n 'options' => $savedSearches,\n ],\n ];\n }\n\n public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array\n {\n $team = $user->getTeam();\n $userTimezone = $user->getTimezone();\n\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n $teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ])->values()->all();\n\n $shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];\n\n $sharedTeamsValue = [];\n $sharedUsersValue = [];\n if ($report) {\n $sharedTeamsValue = $this->transformGroups($team, $report->getGroups());\n\n $recipientUserIds = $report->getRecipients()['users'] ?? [];\n $creatorId = $report->getAttribute('created_by');\n $sharedUserIds = array_values(array_filter(\n $recipientUserIds,\n static fn ($id) => $id !== $creatorId\n ));\n $sharedUsersValue = collect($sharedUserIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (User $u) => [\n 'id' => $u->getUuid(),\n 'name' => $u->getName(),\n ])\n ->values()\n ->all();\n }\n\n return [\n 'fields' => [\n [\n 'id' => 'enabled',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'label' => '',\n 'value' => $report?->getStatus() ?? false,\n ],\n [\n 'id' => 'report_name',\n 'inputType' => InputTypeEnum::TEXT,\n 'label' => 'Name',\n 'placeholder' => 'Enter name',\n 'required' => true,\n 'validation' => ['maxLength' => 50],\n 'value' => $report?->getCustomName() ?? '',\n ],\n [\n 'id' => 'frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Frequency',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::ASK_JIMINNY_FREQUENCIES,\n 'value' => $report ? $this->transformFrequency($report->getFrequency()) : null,\n ],\n [\n 'id' => 'expires_on',\n 'inputType' => InputTypeEnum::DATE,\n 'label' => 'Expires on',\n 'required' => true,\n 'placeholder' => 'Select',\n 'validation' => [\n 'minDate' => now($userTimezone)->toDateString(),\n 'maxDate' => now($userTimezone)->addYear()->toDateString(),\n ],\n 'value' => $report?->getExpiresAt()?->toDateString(),\n ],\n [\n 'id' => 'share_teams',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team',\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $teamGroups,\n 'value' => $sharedTeamsValue,\n ],\n [\n 'id' => 'share_users',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team member',\n 'required' => false,\n 'placeholder' => 'Select',\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'options' => $shareUsers,\n 'value' => $sharedUsersValue,\n ],\n [\n 'id' => 'saved_search',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Saved search',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $savedSearches,\n 'value' => $report && $report->getSavedSearch() ? [\n 'id' => $report->getSavedSearch()->getUuid(),\n 'name' => $report->getSavedSearch()->getName(),\n ] : null,\n ],\n [\n 'id' => 'ask_jiminny_prompt',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Ask Jiminny prompt',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $prompts,\n 'value' => $report && $report->getAskAnythingPrompt() ? [\n 'id' => $report->getAskAnythingPrompt()->getUuid(),\n 'name' => $report->getAskAnythingPrompt()->getTitle(),\n ] : null,\n ],\n ],\n ];\n }\n\n private function updateResultNames(AutomatedReport $automatedReport): void\n {\n $results = $this->automatedReportsRepository->getResultsByReport($automatedReport);\n\n foreach ($results as $result) {\n $result->update(['name' => $this->getReportFileName($result)]);\n }\n }\n\n public function updateStatus(string $uuid, array $data): array\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $automatedReport->update([\n 'status' => $status,\n ]);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n private function generateOneOffReport(AutomatedReport $automatedReport): void\n {\n // the scheduler handles all the other frequency types\n if ($automatedReport->getStatus() === false || $automatedReport->getFrequency() !== self::FREQUENCY_ONE_OFF) {\n return;\n }\n\n $this->dispatcher->dispatch(new RequestGenerateReportJob($automatedReport->getUuid()));\n }\n\n public function getReport(string $uuid, ?Partner $partner = null): AutomatedReport\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n if ($partner !== null && ! $partner->isDefaultPartner() && $automatedReport->team->partner_id !== $partner->getId()) {\n throw new ModelNotFoundException('Report not found');\n }\n\n return $automatedReport;\n }\n\n public function get(string $uuid, ?Partner $partner = null): array\n {\n $automatedReport = $this->getReport($uuid, $partner);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function list(string $sortColumn = 'created_at', string $sortDirection = 'desc', ?Partner $partner = null): array\n {\n $results = [];\n $collection = $this->automatedReportsRepository->getAllStandardReports($sortColumn, $sortDirection, $partner);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function listAskJiminnyReports(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): array {\n $results = [];\n $collection = $this->automatedReportsRepository->getAskJiminnyReportsByUser($user, $sortColumn, $sortDirection);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function delete(string $uuid): void\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $automatedReport->delete();\n }\n\n public function createReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n return $this->automatedReportsRepository->createResult(\n array_merge(\n [\n 'report_id' => $automatedReport->getId(),\n 'status' => AutomatedReportResult::STATUS_DEFAULT,\n ],\n $data\n )\n );\n }\n\n public function getOrCreateReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n $existing = $this->automatedReportsRepository->findLatestSameDayDefaultOrFailedResult($automatedReport);\n\n if ($existing !== null) {\n $existing->update(['status' => AutomatedReportResult::STATUS_DEFAULT]);\n\n return $existing;\n }\n\n return $this->createReportResult($automatedReport, $data);\n }\n\n public function getReportResult(string $resultUuid): AutomatedReportResult\n {\n $report = $this->automatedReportsRepository->findResultByUuid($resultUuid);\n\n if (! $report) {\n throw new ModelNotFoundException('Report Result not found');\n }\n\n return $report;\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return $this->automatedReportsRepository->findChildResult($result, $type);\n }\n\n // prophet API calls\n /**\n * @throws ApplicationException\n */\n public function getGenerateReportPayload(AutomatedReport $automatedReport, string $reportResultUuid): array\n {\n $period = $this->calculateFromAndToDate($automatedReport);\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n return [\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResultUuid,\n 'report_type' => $automatedReport->getType(),\n 'media_types' => $automatedReport->getMediaTypes(),\n 'from_date' => $fromDate->startOfDay()->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->endOfDay()->format(DateTimeInterface::RFC3339),\n 'group_ids' => $automatedReport->getGroups(),\n 'call_deal_stage' => $automatedReport->getDealAtCallStages(),\n 'current_deal_stage' => $automatedReport->getCurrentDealStages(),\n 'deal_min_value' => $automatedReport->getDealValueMin(),\n 'deal_max_value' => $automatedReport->getDealValueMax(),\n 'call_types' => $automatedReport->getCallTypes(),\n 'call_duration_min_seconds' => $automatedReport->getCallDurationMin(),\n 'call_duration_max_seconds' => $automatedReport->getCallDurationMax(),\n 'special_requirements' => $automatedReport->getAdditionalPromptInput(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->formatReportPeriodName(\n $automatedReport->getFrequency(),\n $fromDate,\n $toDate,\n ),\n 'playbook_categories' => $automatedReport->getPlaybookCategories(),\n 'custom_name' => $automatedReport->getCustomName(),\n ];\n }\n\n // $inputPayload - FE payload structure\n public function getActivitiesCountPayload(array $inputPayload): array\n {\n // Use validateAndTransformData to validate and normalize input\n $validatedData = $this->validateAndTransformData($inputPayload);\n $period = $this->calculateFromAndToDatePeriod(\n $validatedData['frequency'],\n Carbon::parse($validatedData['from']),\n Carbon::parse($validatedData['to']),\n );\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n // Create payload similar to getGenerateReportPayload\n return [\n 'team_id' => $validatedData['team_id'],\n 'group_ids' => $validatedData['groups'] ?? [],\n 'report_type' => $validatedData['type'],\n 'from_date' => $fromDate->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->format(DateTimeInterface::RFC3339),\n 'call_deal_stage' => $validatedData['deal_at_call_stages'] ?? [],\n 'current_deal_stage' => $validatedData['current_deal_stages'] ?? [],\n 'deal_min_value' => $validatedData['deal_value_min'] ?? null,\n 'deal_max_value' => $validatedData['deal_value_max'] ?? null,\n 'call_types' => $validatedData['call_types'],\n 'call_duration_min_seconds' => $validatedData['call_duration_min'] ?? null,\n 'call_duration_max_seconds' => $validatedData['call_duration_max'] ?? null,\n 'special_requirements' => $validatedData['additional_prompt_input'] ?? null,\n 'playbook_categories' => $validatedData['playbook_categories'] ?? [],\n 'request_id' => null,\n 'callback_url' => null,\n ];\n }\n\n public function shouldSendReport(array $users, ?CarbonInterface $generatedAt = null): bool\n {\n if (empty($users)) {\n return false;\n }\n\n $earliestTz = collect($users)\n ->mapWithKeys(function (array $user) {\n $tz = new DateTimeZone($user['timezone']);\n $nowUtc = new DateTime('now', new DateTimeZone('UTC'));\n $offset = $tz->getOffset($nowUtc);\n\n return [$user['timezone'] => $offset];\n })\n ->sortDesc()\n ->keys()\n ->first();\n\n $now = Carbon::now($earliestTz);\n $isScheduledTime = (int) $now->format('H') === self::SENT_REPORT_AT_HOURS;\n\n if ($isScheduledTime) {\n return true;\n }\n\n return $this->hasPassedScheduledTime($generatedAt, $earliestTz);\n }\n\n public function hasPassedScheduledTime(?CarbonInterface $generatedAt, string $timezone): bool\n {\n if ($generatedAt === null) {\n return false;\n }\n\n $now = Carbon::now($timezone);\n $scheduledTime = $now->copy()->setTime(self::SENT_REPORT_AT_HOURS, 0, 0);\n\n if ($now->hour < self::SENT_REPORT_AT_HOURS) {\n $scheduledTime = $scheduledTime->subDay();\n }\n\n $scheduledTimeUtc = $scheduledTime->copy()->utc();\n $generatedAtUtc = $generatedAt->copy()->utc();\n $nowUtc = $now->copy()->utc();\n\n return $generatedAtUtc->lt($scheduledTimeUtc) && $nowUtc->gt($scheduledTimeUtc);\n }\n\n public function calculateFromAndToDatePeriod(\n string $frequency,\n ?Carbon $fromDate = null,\n ?Carbon $toDate = null,\n DateTimeZone|string|null $timezone = 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($timezone);\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()->subWeek()->startOfWeek(CarbonInterface::MONDAY),\n 'toDate' => $now->copy()->subWeek()->endOfWeek(CarbonInterface::SUNDAY),\n ],\n self::FREQUENCY_MONTHLY => [\n 'fromDate' => $now->copy()->subMonthNoOverflow()->startOfMonth(),\n 'toDate' => $now->copy()->subMonthNoOverflow()->endOfMonth(),\n ],\n self::FREQUENCY_QUARTERLY => [\n 'fromDate' => $now->copy()->subQuarterNoOverflow()->startOfQuarter(),\n 'toDate' => $now->copy()->subQuarterNoOverflow()->endOfQuarter(),\n ],\n default => throw new InvalidArgumentException(\"Unsupported frequency: {$frequency}\"),\n };\n }\n\n private function calculateFromAndToDate(AutomatedReport $automatedReport): array\n {\n return $this->calculateFromAndToDatePeriod(\n $automatedReport->getFrequency(),\n $automatedReport->getFrom(),\n $automatedReport->getTo()\n );\n }\n\n public function getAskJiminnyGenerateReportPayload(\n AutomatedReport $automatedReport,\n AutomatedReportResult $reportResult,\n array $activityIds,\n ): array {\n return [\n 'user_question' => $automatedReport->getAskAnythingPrompt()?->getContent(),\n 'call_ids' => array_map('strval', $activityIds),\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResult->getUuid(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->getReportPeriodName($reportResult),\n 'report_name' => $automatedReport->getCustomName(),\n ];\n }\n\n private function getCallbackUrl(): string\n {\n return $this->webhookService->route('jiminny.webhook.reports.ready');\n }\n\n /**\n * Validate and transform payload data for automated reports\n *\n * @param array $data\n *\n * @throws InvalidArgumentException\n *\n * @return array\n */\n private function validateAndTransformData(array $data): array\n {\n // Validate organization (team) and check feature\n $team = $this->validateOrganization($data['organization'] ?? null);\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $type = $this->validateReportType($data['report_type'] ?? null);\n $frequency = $this->validateFrequency($data['frequency'] ?? null);\n $additionalPromptInput = $this->validateAdditionalPromptInput(\n $data['additional_prompt_input'] ?? null\n );\n $customReportName = $this->validateCustomReportName($data['custom_name'] ?? null);\n\n // Prepare data for the database\n $reportData = [\n 'team_id' => $team->getId(),\n 'type' => $type,\n 'status' => $status,\n 'frequency' => $frequency,\n 'additional_prompt_input' => $additionalPromptInput,\n 'custom_name' => $customReportName,\n ];\n\n // Validate deal values\n $reportData = $this->validateDealValues($data, $reportData);\n\n // Validate date range\n $reportData = $this->validateDateRange($data, $reportData, $frequency);\n\n // Validate call durations\n $reportData = $this->validateCallDurations($data, $reportData);\n\n // Validate call types\n $reportData = $this->validateCallTypes($data, $reportData);\n\n // Validate media types\n $reportData = $this->validateMediaTypes($data, $reportData);\n\n // Validate groups\n if (isset($data['teams'])) {\n $reportData['groups'] = $this->validateAndGetGroupIds($team, $data['teams']);\n }\n\n // Validate deal stages\n $reportData = $this->validateDealStages($data, $reportData, $team, $type);\n\n // Validate playbook categories\n $reportData = $this->validatePlaybookCategories($data, $reportData, $team);\n\n // Validate recipients\n $reportData['recipients'] = [\n 'users' => $this->validateAndGetUserIdsByTeam($team, $data['recipients'] ?? []),\n ];\n\n if (isset($data['jiminny_recipients'])) {\n // Validate Jiminny recipients\n $reportData['jiminny_recipients'] = [\n 'users' => $this->validateAndGetJiminnyUserIds((array) $data['jiminny_recipients']),\n ];\n }\n\n return $reportData;\n }\n\n private function validateDealValues(array $data, array $reportData): array\n {\n if (isset($data['min_deal_value'])) {\n $reportData['deal_value_min'] = (int) $data['min_deal_value'];\n\n if ($reportData['deal_value_min'] > 4294967295 || $reportData['deal_value_min'] < 0) {\n throw new InvalidArgumentException('Min deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_deal_value'])) {\n $reportData['deal_value_max'] = (int) $data['max_deal_value'];\n\n if ($reportData['deal_value_max'] > 4294967295 || $reportData['deal_value_max'] < 0) {\n throw new InvalidArgumentException('Max deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['min_deal_value'], $data['max_deal_value'])\n && $data['min_deal_value'] > $data['max_deal_value']\n ) {\n throw new InvalidArgumentException('Min deal value cannot be greater than max deal value');\n }\n\n return $reportData;\n }\n\n private function validateDateRange(array $data, array $reportData, string $frequency): array\n {\n // Set date range only for one_off frequency\n if ($frequency === 'one_off') {\n if (isset($data['start_date_period'])) {\n $reportData['from'] = $this->parseDate($data['start_date_period']);\n }\n\n if (isset($data['end_date_period'])) {\n $reportData['to'] = $this->parseDate($data['end_date_period']);\n }\n\n if (empty($reportData['from']) || empty($reportData['to'])) {\n throw new InvalidArgumentException(\n 'Start date and end date are required for one_off frequency'\n );\n }\n } else {\n $reportData['from'] = null;\n $reportData['to'] = null;\n }\n\n return $reportData;\n }\n\n private function validateCallDurations(array $data, array $reportData): array\n {\n // Convert call durations from minutes to seconds\n if (isset($data['min_call_duration'])) {\n $reportData['call_duration_min'] = (int) $data['min_call_duration'] * 60;\n\n if ($reportData['call_duration_min'] > 4294967295 || $reportData['call_duration_min'] < 0) {\n throw new InvalidArgumentException('Min call duration should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_call_duration'])) {\n $reportData['call_duration_max'] = (int) $data['max_call_duration'] * 60;\n\n if ($reportData['call_duration_max'] > 4294967295 || $reportData['call_duration_max'] < 0) {\n throw new InvalidArgumentException('Max call duration should be between 0 and 4294967295');\n }\n }\n\n return $reportData;\n }\n\n private function validateCallTypes(array $data, array $reportData): array\n {\n // Set call types\n $reportData['call_types'] = $data['call_type'] ?? [];\n if (empty($reportData['call_types'])) {\n $reportData['call_types'] = self::getCallTypes();\n }\n\n foreach ($reportData['call_types'] as $callType) {\n if (! in_array($callType, self::getCallTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Call type %s is invalid', $callType));\n }\n }\n\n return $reportData;\n }\n\n private function validateMediaTypes(array $data, array $reportData): array\n {\n // Set media types from input data\n $reportData['media_types'] = $data['media_types'] ?? [];\n\n if (empty($reportData['media_types'])) {\n throw new InvalidArgumentException('Media types are required');\n }\n\n foreach ($reportData['media_types'] as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n throw new InvalidArgumentException(sprintf('Media type %s is invalid', $mediaType));\n }\n }\n\n return $reportData;\n }\n\n private function validateDealStages(array $data, array $reportData, Team $team, string $reportType): array\n {\n // Validate and set deal stages\n if (isset($data['deal_stage_at_call'])) {\n $reportData['deal_at_call_stages'] =\n $this->validateAndGetDealStageIds($team, $data['deal_stage_at_call'], 'Deal stage at call');\n }\n\n if (isset($data['current_deal_stage'])) {\n $reportData['current_deal_stages'] =\n $this->validateAndGetDealStageIds($team, $data['current_deal_stage'], 'Current deal stage');\n }\n\n // Ensure current_deal_stage is not provided for loss_analysis report type\n if ($reportType === self::TYPE_LOSS_ANALYSIS && ! empty($data['current_deal_stage'])) {\n throw new InvalidArgumentException('Current deal stage is not applicable for Loss Analysis reports');\n }\n\n return $reportData;\n }\n\n // transform uuid to id\n private function validatePlaybookCategories(array $data, array $reportData, Team $team): array\n {\n $key = 'playbook_categories';\n\n if (isset($data[$key])) {\n $payloadIds = $data[$key];\n $ids = [];\n\n foreach ($payloadIds as $uuid) {\n $uuid = (string) $uuid;\n\n try {\n $playbookCategory = $this->playbookCategoryRepository->findByUuid($uuid);\n } catch (Throwable $throwable) {\n Log::error(__METHOD__ . ' ' . $throwable->getMessage());\n\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory) {\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory->hasPlaybook()) {\n throw new InvalidArgumentException(sprintf('Playbook category %s has no playbook', $uuid));\n }\n\n if ($playbookCategory->getPlaybook()->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Playbook category %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $ids[] = $playbookCategory->getId();\n }\n\n $reportData[$key] = $ids;\n }\n\n return $reportData;\n }\n\n private function validateReportStatus($status): bool\n {\n if (! in_array($status, [true, false], true)) {\n throw new InvalidArgumentException('Report status is invalid');\n }\n\n return $status;\n }\n\n private function validateReportType($type): string\n {\n if (! in_array($type, self::getTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Report type is invalid: %s', $type));\n }\n\n return $type;\n }\n\n private function validateFrequency($frequency): string\n {\n if (! in_array($frequency, self::getFrequencies(), true)) {\n throw new InvalidArgumentException('Frequency is invalid');\n }\n\n return $frequency;\n }\n\n private function validateAdditionalPromptInput(?string $additionalPromptInput): ?string\n {\n if ($additionalPromptInput && strlen($additionalPromptInput) > 5000) {\n throw new InvalidArgumentException('Additional Prompt Input should be less than 5000 characters');\n }\n\n return $additionalPromptInput;\n }\n\n private function validateCustomReportName(?string $customReportName): ?string\n {\n if ($customReportName === null || $customReportName === '') {\n return null;\n }\n\n if (strlen($customReportName) > 70) {\n throw new InvalidArgumentException('Custom report name should be less than 70 characters');\n }\n\n return $customReportName;\n }\n\n private function validateOrganization(?string $organizationUuid): Team\n {\n if (! $organizationUuid) {\n throw new InvalidArgumentException('Organization is required');\n }\n\n $team = $this->teamRepository->idOrUuid($organizationUuid);\n\n if (! $team) {\n throw new InvalidArgumentException('Organization not found');\n }\n\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n throw new InvalidArgumentException('Organization does not have the Automated Reports feature');\n }\n\n return $team;\n }\n\n private function validateAndGetGroupIds(Team $team, array $teamUuids): array\n {\n $groupIds = [];\n\n foreach ($teamUuids as $uuid) {\n $group = $this->groupRepository->findByUuid($uuid);\n\n if ($group === null || $group->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Group %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $groupIds[] = $group->getId();\n\n }\n\n return $groupIds;\n }\n\n private function validateAndGetDealStageIds(Team $team, array $stageUuids, string $propertyLabel): array\n {\n $stageIds = [];\n\n foreach ($stageUuids as $uuid) {\n $stage = $this->stageRepository->findByUuid($uuid);\n\n if ($stage === null || $stage->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Stage %s not found for team %s for %s', $uuid, $team->getUuid(), $propertyLabel)\n );\n }\n\n $stageIds[] = $stage->getId();\n }\n\n return $stageIds;\n }\n\n private function validateAndGetUserIds(array $userUuids, callable $teamCheck): array\n {\n if (empty($userUuids)) {\n return [];\n }\n\n $userIds = [];\n\n foreach ($userUuids as $uuid) {\n $user = $this->userRepository->findByUuid($uuid);\n\n if (! $user || ! $user->isStatusActive()) {\n throw new InvalidArgumentException(\n sprintf('User %s not found or is not active', $uuid)\n );\n }\n\n if (! $teamCheck($user)) {\n throw new InvalidArgumentException(\n sprintf('User %s does not belong to the allowed team(s)', $uuid)\n );\n }\n\n $userIds[] = $user->getId();\n }\n\n return $userIds;\n }\n\n private function validateAndGetUserIdsByTeam(Team $team, array $userUuids): array\n {\n return $this->validateAndGetUserIds($userUuids, fn ($user) => $user->getTeamId() === $team->getId());\n }\n\n private function validateAndGetJiminnyUserIds(array $userUuids): array\n {\n $allowedTeamIds = config('kiosk.teamIds', []);\n\n return $this->validateAndGetUserIds($userUuids, fn ($user) => in_array($user->getTeamId(), $allowedTeamIds, true));\n }\n\n private function parseDate(string $dateString): string\n {\n return date('Y-m-d H:i:s', strtotime($dateString));\n }\n\n private function generateReportResultViewUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.view', ['uuid' => $result->getUuid()]);\n }\n\n private function generateReportResultDownloadUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.download', ['uuid' => $result->getUuid()]);\n }\n\n private function getReportMediaRouteResource(AutomatedReportResult $result): string\n {\n if ($result->getMediaType() === self::MEDIA_TYPE_PDF) {\n return self::PDF_KEY;\n } elseif ($result->getMediaType() === self::MEDIA_TYPE_PODCAST) {\n return self::AUDIO_KEY;\n }\n\n throw new \\InvalidArgumentException('Unknown media type.');\n }\n\n public function getMediaPath(AutomatedReportResult $result): ?string\n {\n $url = match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => $result->getPdfUrl(),\n self::MEDIA_TYPE_PODCAST => $result->getPodcastAudioUrl(),\n default => null,\n };\n\n if ($url === null) {\n return null;\n }\n\n $path = parse_url(trim($url, '\"\\''), PHP_URL_PATH);\n\n return $path ?: null;\n }\n\n public function getFilenameSuffix(AutomatedReportResult $result): ?string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => 'Podcast',\n default => null,\n };\n }\n\n public function getMailSubjectSuffix(AutomatedReportResult $result): string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => 'report',\n self::MEDIA_TYPE_PODCAST => 'podcast',\n default => '',\n };\n }\n\n public function getMediaTypeMetadata(AutomatedReportResult $result): array\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => ['extension' => 'mp3', 'mime' => 'audio/mpeg'],\n self::MEDIA_TYPE_PDF => ['extension' => 'pdf', 'mime' => 'application/pdf'],\n default => ['extension' => null, 'mime' => null],\n };\n }\n\n public function deleteS3Files(AutomatedReportResult $result): void\n {\n $teamUuid = $result->getReport()->getTeam()->getUuid();\n $reportUuid = $result->getUuid();\n\n // delete all files for a report uuid no mather of pdf, podcast, or both\n // in case of both - the podcast files are linked to the pdf (parent) uuid\n // pdf and podcast date times should be close\n $path = sprintf('%s/%s/%s', $teamUuid, self::S3_DIR, $reportUuid);\n\n foreach (self::FILE_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted S3 file', [\n 'path' => $file,\n ]);\n }\n }\n\n foreach (self::FILE_PODCAST_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '_podcast.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted Podcast S3 file', [\n 'path' => $file,\n ]);\n }\n }\n }\n\n /**\n *\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): Collection\n {\n return $this->automatedReportsRepository->getTeamIdsWithReportsResults($teamId);\n }\n\n /**\n * Core delete logic for report results using a query\n *\n * @param Builder $query\n * @param array $logContext\n *\n * @return int\n */\n private function deleteReportResultsByQuery(Builder $query, array $logContext = []): int\n {\n $deletedCount = 0;\n\n if ($query->exists()) {\n Log::info(\n 'Run delete report results',\n array_merge(\n $logContext,\n [\n 'service' => 'AutomatedReportsService',\n ]\n )\n );\n\n $query->chunkById(50, function ($results) use (&$deletedCount, $logContext) {\n foreach ($results as $result) {\n $this->deleteReportResult($result);\n $deletedCount++;\n\n Log::info(\n 'Deleted a report result',\n array_merge(\n $logContext,\n [\n 'result_id' => $result->getId(),\n 'report_id' => $result->getReportId(),\n ]\n )\n );\n }\n });\n }\n\n return $deletedCount;\n }\n\n /**\n * Delete report results for a team by retention period\n *\n * @param Team $team\n * @param CarbonImmutable $retentionDate\n *\n * @return int Number of deleted report results\n */\n public function deleteReportsResultsInRetentionPeriod(Team $team, CarbonImmutable $retentionDate): int\n {\n $reportIds = $this->automatedReportsRepository->getReportIdsByTeam($team);\n\n if ($reportIds->isEmpty()) {\n return 0;\n }\n\n $query = $this->automatedReportsRepository\n ->getReportResultsQueryForRetention($team, $retentionDate);\n\n return $this->deleteReportResultsByQuery($query, [\n 'team_id' => $team->getId(),\n 'retention_date' => $retentionDate->toDateTimeString(),\n ]);\n }\n\n /**\n * Delete ALL report results for a specific automated report\n *\n * @param string $uuid\n *\n * @return int\n */\n public function deleteReportResults(string $uuid): int\n {\n $report = $this->getReport($uuid);\n\n $query = $this->automatedReportsRepository->getResultsByReportQuery($report);\n\n return $this->deleteReportResultsByQuery($query, [\n 'report_uuid' => $uuid,\n 'report_id' => $report->getId(),\n ]);\n }\n\n public function deleteReportResult(AutomatedReportResult $result): void\n {\n $this->deleteS3Files($result);\n\n $result->delete();\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getTeamReports(Team $team): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getReportsByTeam($team);\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getReportResults(AutomatedReport $report): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getResultsByReport($report);\n }\n\n public function deleteAllReportResults(AutomatedReport $report): void\n {\n $results = $this->getReportResults($report);\n\n /** @var AutomatedReportResult $result */\n foreach ($results as $result) {\n Log::info('Deleting result', [\n 'report' => $report->getId(),\n 'result' => $result->getId(),\n ]);\n\n $this->deleteReportResult($result);\n }\n }\n\n public function deleteAllData(Team $team): void\n {\n Log::info('Deleting automated report and results for team', [\n 'team' => $team->getId(),\n ]);\n\n $reports = $this->getTeamReports($team);\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n Log::info('Deleting report', [\n 'team' => $team->getId(),\n 'report' => $report->getId(),\n ]);\n\n $this->deleteAllReportResults($report);\n\n $report->delete();\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Carbon\\CarbonImmutable;\nuse Carbon\\CarbonInterface;\nuse Carbon\\Exceptions\\InvalidFormatException;\nuse DateTime;\nuse DateTimeInterface;\nuse DateTimeZone;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\InputTypeEnum;\nuse Jiminny\\Component\\AskAnything\\AskAnythingPromptService;\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Contracts\\Repositories\\PlaybookCategoryRepository;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\ModelNotFoundException;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Partner;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Repositories\\GroupRepository;\nuse Jiminny\\Repositories\\SearchRepository;\nuse Jiminny\\Repositories\\StageRepository;\nuse Throwable;\n\nclass AutomatedReportsService\n{\n public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';\n public const string TYPE_ASK_JIMINNY = 'ask_jiminny';\n\n /**\n * Standard report types (used by kiosk for existing automated reports).\n */\n // @TODO this will add filter, however if we need to control feature by FF we need conditional logic\n public const array TYPES = [\n ['id' => 'exec_summary', 'name' => 'Exec Summary'],\n ['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],\n ['id' => 'product_feedback', 'name' => 'Product Feedback'],\n ['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],\n// ['id' => 'questions', 'name' => 'Questions'],\n// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],\n ];\n\n public const array ALL_TYPES = [\n ...self::TYPES,\n ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],\n ];\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\n /**\n * Frequencies for standard (non-Ask Jiminny) reports.\n */\n public const array FREQUENCIES = [\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n\n /**\n * Frequencies for Ask Jiminny reports.\n */\n public const array ASK_JIMINNY_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ];\n\n public const string MEDIA_TYPE_PDF = 'pdf';\n public const string MEDIA_TYPE_PODCAST = 'podcast';\n public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];\n public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];\n public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];\n public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];\n\n public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];\n public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];\n public const int SENT_REPORT_AT_HOURS = 5;\n public const string PDF_KEY = 'pdf';\n public const string AUDIO_KEY = 'audio';\n\n private const array ALL_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n private const string S3_DIR = 'reports';\n private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];\n private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];\n\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly GroupRepository $groupRepository,\n private readonly UserRepository $userRepository,\n private readonly StageRepository $stageRepository,\n private readonly DealStagesService $dealStagesService,\n private readonly RecipientsService $recipientsService,\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly Webhook $webhookService,\n private readonly BusDispatcher $dispatcher,\n private readonly ActivityTypeService $activityTypeService,\n private readonly PlaybookCategoryRepository $playbookCategoryRepository,\n private readonly AskAnythingPromptService $askAnythingPromptService,\n private readonly SearchRepository $activitySearchRepository,\n private readonly AskAnythingRepository $askAnythingRepository,\n ) {\n }\n\n public static function getTypes(): array\n {\n $types = self::TYPES;\n\n return array_map(static function ($type) {\n return $type['id'];\n }, $types);\n }\n\n public static function getCallTypes(): array\n {\n return array_map(static function ($callType) {\n return $callType['id'];\n }, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);\n }\n\n public static function getFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::FREQUENCIES);\n }\n\n // front-facing structure\n public function getReportEnabledFieldData(bool $value = false): array\n {\n return [\n 'id' => 'report_enabled',\n 'label' => '',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'value' => $value,\n ];\n }\n\n // Organizations = Teams\n public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array\n {\n $options = $this->getTeams(partner: $partner);\n\n if ($shortVersion) {\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value,\n 'dependencies' => [\n 'teams',\n 'deal_stage_at_call',\n 'current_deal_stage',\n 'recipients',\n ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,\n ],\n 'dependsOn' => [],\n ];\n }\n\n // Teams = Groups\n public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array\n {\n if ($shortVersion) {\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value, // value should be an array of objects {id, name}\n 'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],\n 'dependsOn' => [],\n ];\n }\n\n public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array\n {\n $types = [];\n if ($team instanceof Team) {\n if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n $types = self::TYPES;\n }\n if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {\n $types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];\n }\n } else {\n $types = self::TYPES;\n }\n\n if ($shortVersion) {\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'options' => $types,\n ];\n }\n\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $types,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getFrequencyFieldData(?string $value = null): array\n {\n return [\n 'id' => 'frequency',\n 'label' => 'Frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::FREQUENCIES,\n 'value' => $value,\n 'dependencies' => ['period'],\n 'dependsOn' => [],\n ];\n }\n\n public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array\n {\n return [\n 'id' => 'period',\n 'label' => 'Select one-off period',\n 'inputType' => InputTypeEnum::DATE_RANGE,\n 'required' => true,\n 'placeholder' => 'Select',\n 'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],\n 'queryParams' => [\n 'startDate' => 'start_date_period',\n 'endDate' => 'end_date_period',\n ],\n 'dependencies' => [],\n 'dependsOn' => ['frequency'],\n ];\n }\n\n public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array\n {\n return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);\n }\n\n public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);\n }\n\n public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);\n }\n\n public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'deal_value',\n 'label' => 'Deal Value',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_deal_value',\n 'max' => 'max_deal_value',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array\n {\n $value = [];\n $conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;\n $dialerOn && $value[] = self::CALL_TYPE_DIALER;\n\n return [\n 'id' => 'call_type',\n 'label' => 'Call Type',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => [\n self::CALL_TYPE_CONFERENCE,\n self::CALL_TYPE_DIALER,\n ],\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getMediaTypeFieldData(?AutomatedReport $report = null): array\n {\n $value = [];\n\n if ($report) {\n $value = $this->transformMediaTypes($report);\n }\n\n return [\n 'id' => 'media_types',\n 'label' => 'Export as',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => self::MEDIA_TYPE_OBJECTS,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'call_duration',\n 'label' => 'Call Duration',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_call_duration',\n 'max' => 'max_call_duration',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getRecipientsFieldData(?Team $team = null, array $value = []): array\n {\n return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);\n }\n\n public function getJiminnyRecipientsFieldData(array $value = []): array\n {\n return $this->recipientsService->getJiminnyRecipientsFieldData($value);\n }\n\n public function getAdditionalPromptInputFieldData(?string $value = null): array\n {\n return [\n 'id' => 'additional_prompt_input',\n 'label' => 'Special requirements',\n 'inputType' => InputTypeEnum::TEXTAREA,\n 'required' => false,\n 'placeholder' => 'What should be the focus of the report?',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCustomReportNameFieldData(?string $value = null): array\n {\n return [\n 'id' => 'custom_name',\n 'label' => 'Custom report name',\n 'inputType' => InputTypeEnum::TEXT,\n 'required' => false,\n 'placeholder' => 'Enter custom name',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n // data providers\n public function getTeams(?Partner $partner = null): array\n {\n $teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);\n\n $teamData = [];\n foreach ($teams as $team) {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n continue;\n }\n\n $teamData[] = $this->transformTeam($team);\n }\n\n return $teamData;\n }\n\n public function getTeamGroups(string $teamUuid): array\n {\n $data = [];\n $team = $this->getTeam($teamUuid);\n\n if ($team !== null) {\n $groups = $team->groups()->get();\n\n foreach ($groups as $group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array\n {\n $data = [];\n $teams = $this->getTeams(partner: $partner);\n\n foreach ($teams as $team) {\n if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {\n continue;\n }\n\n $data[] = [\n 'label' => $team['name'],\n 'groups' => $this->getTeamGroups($team['id']),\n ];\n }\n\n return $data;\n }\n\n public function getTeam(string $teamUuid): ?Team\n {\n return $this->teamRepository->idOrUuid($teamUuid);\n }\n\n public function getTeamById(int $teamId): ?Team\n {\n return $this->teamRepository->find($teamId);\n }\n\n public function getGroupsUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportGroups = $report->getGroups();\n foreach ($reportGroups as $groupId) {\n if ($group = $this->groupRepository->find($groupId)) {\n $uuids[] = $group->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getPlaybookCategoriesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $playbookCategories = $report->getPlaybookCategories();\n foreach ($playbookCategories as $id) {\n if ($category = $this->playbookCategoryRepository->find($id)) {\n $uuids[] = $category->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getDealAtCallStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getDealAtCallStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getCurrentDealStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getCurrentDealStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getRecipients());\n }\n\n public function getJiminnyUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function extractUserUuids(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => $user->getUuid())\n ->values()\n ->all();\n }\n\n // get mail data\n public function getRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getRecipients());\n }\n\n /**\n * @return array<UserContract>\n */\n public function getRecipientUserObjects(AutomatedReport $report): array\n {\n $userIds = $report->getRecipients()['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->values()\n ->all();\n }\n\n private function getJiminnyRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function buildRecipientUsers(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => [\n 'email' => $user->getEmailAddress(),\n 'name' => $user->getName(),\n 'timezone' => $user->getTimezone()->getName(),\n ])\n ->values()\n ->all();\n }\n\n public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array\n {\n if ($report->isAskJiminnyReport()) {\n $recipients = $this->resolveAskJiminnyRecipients($report);\n } else {\n $recipients = $this->getRecipientUsers($report);\n if ($includeJiminny) {\n $recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));\n }\n }\n\n $emails = [];\n\n return array_values(array_filter(\n $recipients,\n static function ($recipient) use (&$emails) {\n if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {\n return false;\n }\n $emails[] = $recipient['email'];\n\n return true;\n }\n ));\n }\n\n private function resolveAskJiminnyRecipients(AutomatedReport $report): array\n {\n $recipients = [];\n\n $creator = $report->getCreator();\n if ($creator !== null) {\n $recipients[] = [\n 'email' => $creator->getEmailAddress(),\n 'name' => $creator->getName(),\n 'timezone' => $creator->getTimezone()->getName(),\n ];\n }\n\n return array_merge(\n $recipients,\n $this->buildRecipientUsers($report->getRecipients()),\n $this->getGroupRecipientUsers($report),\n );\n }\n\n private function getGroupRecipientUsers(AutomatedReport $report): array\n {\n $users = [];\n foreach ($report->getGroups() as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group === null) {\n continue;\n }\n foreach ($group->getMembers() as $member) {\n $users[] = [\n 'email' => $member->getEmailAddress(),\n 'name' => $member->getName(),\n 'timezone' => $member->getTimezone()->getName(),\n ];\n }\n }\n\n return $users;\n }\n\n public function getReportTypeName(AutomatedReportResult $report): string\n {\n $type = $report->getReport()->getType();\n\n $getType = $this->transformReportType($type);\n\n return $getType['name'];\n }\n\n public function getReportPeriodName(AutomatedReportResult $report): string\n {\n $from = $report->getFromDate();\n $to = $report->getToDate();\n $frequency = $report->getReport()->getFrequency();\n\n if ($from === null || $to === null) {\n if (! $report->getReport()->isAskJiminnyReport()) {\n $invalidPeriod = $from === null ? 'from' : 'to';\n\n throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);\n }\n\n $timezone = $report->getReport()->getCreator()?->getTimezone();\n $period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);\n $from = $period['fromDate'];\n $to = $period['toDate'];\n }\n\n return $this->formatReportPeriodName($frequency, $from, $to);\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 getReportTeamsName(AutomatedReportResult $report): string\n {\n $groups = $report->getGroups();\n\n if (empty($groups)) {\n return 'All';\n }\n\n // Get group names from repository\n $groupNames = [];\n foreach ($groups as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group) {\n $groupNames[] = $group->getName();\n }\n }\n\n if (count($groupNames) === 1) {\n // Single team format\n $teamsName = $groupNames[0];\n } else {\n // Multiple teams format\n $teamsName = implode(', ', $groupNames);\n }\n\n return $teamsName;\n }\n\n public function getReportFileName(AutomatedReportResult $report): string\n {\n $customName = $report->getReport()->getCustomName();\n $periodName = $this->getReportPeriodName($report);\n $filenameSuffix = $this->getFilenameSuffix($report);\n\n if ($customName) {\n if ($filenameSuffix) {\n $customName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$customName} - {$periodName}\");\n }\n\n $baseName = $this->getReportTypeName($report);\n\n if ($filenameSuffix) {\n $baseName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}\");\n }\n\n public function getReportFileNameWithExtension(AutomatedReportResult $result): string\n {\n $extension = $this->getMediaTypeMetadata($result)['extension'];\n\n return $this->getReportFileName($result) . '.' . $extension;\n }\n\n public function sanitizeFileName(string $fileName): string\n {\n return str_replace(['/', '\\\\'], '-', $fileName);\n }\n\n public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool\n {\n $recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);\n if (in_array($user->getId(), $recipientIds, true)) {\n return true;\n }\n\n if ($report->isAskJiminnyReport()) {\n $groupId = $user->getGroupId();\n if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {\n return true;\n }\n }\n\n return false;\n }\n\n public function transformReportResults(Collection $automatedReportResults): array\n {\n $data = [];\n foreach ($automatedReportResults as $automatedReportResult) {\n /** @var AutomatedReportResult $automatedReportResult */\n\n $report = $automatedReportResult->getReport();\n\n $createdBy = $report->getCreator();\n $creator = [\n 'id' => $createdBy?->getUuid(),\n 'name' => $createdBy?->getName(),\n 'email' => $createdBy?->getEmailAddress(),\n 'photoUrl' => $createdBy?->getPhotoUrl(),\n ];\n\n $data[] = [\n 'id' => $automatedReportResult->getUuid(),\n 'name' => $automatedReportResult->getName(),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n 'recipients' => $this->buildRecipients($report),\n 'report_type' => $this->transformReportType($report->getType()),\n 'media_type' => $automatedReportResult->getMediaType(),\n 'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),\n 'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),\n 'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),\n 'creator' => $creator,\n ];\n }\n\n return $data;\n }\n\n private function buildRecipients(AutomatedReport $report): array\n {\n $creatorUuid = $report->getCreator()?->getUuid();\n\n $recipients = array_values(array_filter(\n $this->transformRecipients($report->getRecipients()),\n static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,\n ));\n\n if (! $report->isAskJiminnyReport()) {\n return $recipients;\n }\n\n return [\n ...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),\n ...$recipients,\n ];\n }\n\n public function hasCallTypeConference(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);\n }\n\n public function hasCallTypeDialer(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);\n }\n\n // transformers\n private function transformTeam(Team $team): array\n {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n return [];\n }\n\n return [\n 'id' => $team->getUuid(),\n 'name' => $team->getName(),\n ];\n }\n\n private function transformReportFullView(AutomatedReport $report): array\n {\n $base = $this->transformReportBase($report);\n\n return $report->getType() === self::TYPE_ASK_JIMINNY\n ? $base + $this->transformAskJiminnyFields($report)\n : $base + $this->transformStandardReportFields($report);\n }\n\n private function transformReportBase(AutomatedReport $report): array\n {\n return [\n 'id' => $report->getUuid(),\n 'organization' => $this->transformOrganization(team: $report->getTeam()),\n 'report_type' => $this->transformReportType($report->getType()),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n ];\n }\n\n private function transformStandardReportFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n\n return [\n 'report_enabled' => $report->getStatus(),\n 'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),\n 'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),\n 'deal_value_min' => $report->getDealValueMin(),\n 'deal_value_max' => $report->getDealValueMax(),\n 'call_types' => $this->transformCallType($report->getCallTypes()),\n 'media_types' => $this->transformMediaTypes($report),\n 'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),\n 'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),\n 'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),\n 'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),\n 'recipients' => $this->transformRecipients($report->getRecipients()),\n 'created_by' => $this->transformCreator($report->getCreator()),\n 'additional_prompt_input' => $report->getAdditionalPromptInput(),\n 'custom_name' => $report->getCustomName(),\n 'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),\n 'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),\n 'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),\n ];\n }\n\n private function transformAskJiminnyFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n $creatorId = $report->getAttribute('created_by');\n $explicitUserIds = array_values(array_filter(\n $report->getRecipients()['users'] ?? [],\n static fn ($id) => $id !== $creatorId\n ));\n\n return [\n 'report_name' => $report->getCustomName(),\n 'enabled' => $report->getStatus(),\n 'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),\n 'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),\n 'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),\n 'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),\n ];\n }\n\n private function transformOrganization(?Team $team): array\n {\n return [\n 'id' => $team?->getUuid(),\n 'name' => $team?->getName(),\n ];\n }\n\n private function transformReportType(string $type): array\n {\n foreach (self::ALL_TYPES as $typeItem) {\n if ($typeItem['id'] === $type) {\n return $typeItem;\n }\n }\n\n return [];\n }\n\n private function transformCallType(array $types): array\n {\n $result = [];\n $callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];\n\n foreach ($types as $type) {\n foreach ($callTypes as $callTypeItem) {\n if ($callTypeItem['id'] === $type) {\n $result[] = $callTypeItem;\n\n break;\n }\n }\n }\n\n return $result;\n }\n\n private function transformMediaTypes(AutomatedReport $report): array\n {\n $values = [];\n\n foreach ($report->getMediaTypes() as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n continue;\n }\n\n $values[] = match ($mediaType) {\n self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,\n self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,\n };\n }\n\n return $values;\n }\n\n private function transformFrequency(string $frequency): array\n {\n foreach (self::ALL_FREQUENCIES as $frequencyItem) {\n if ($frequencyItem['id'] === $frequency) {\n return $frequencyItem;\n }\n }\n\n return [];\n }\n\n public function transformDurationToMinutes(?int $duration): ?int\n {\n if (! $duration) {\n return null;\n }\n\n return (int) ($duration / 60);\n }\n\n private function transformGroups(?Team $team, array $groupsIds): array\n {\n if (empty($groupsIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($groupsIds as $groupId) {\n $group = $team->groups()->where('id', $groupId)->first();\n\n if ($group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n 'photoUrl' => $group->getPhotoUrl(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformStages(?Team $team, array $stagesIds): array\n {\n if (empty($stagesIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($stagesIds as $stageId) {\n $stage = $team->stages()->where('id', $stageId)->first();\n\n if ($stage) {\n $data[] = [\n 'id' => $stage->getUuid(),\n 'name' => $stage->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformRecipients(array $recipients): array\n {\n $users = [];\n foreach ($recipients['users'] ?? [] as $userId) {\n $users[] = $this->transformUser($userId);\n }\n\n return $users;\n }\n\n private function transformCreator(?User $user): ?array\n {\n if ($user === null) {\n return null;\n }\n\n return $this->transformUser($user->getId());\n }\n\n private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array\n {\n if ($prompt === null) {\n return null;\n }\n\n return [\n 'id' => $prompt->getUuid(),\n 'name' => $prompt->getTitle(),\n ];\n }\n\n private function transformSafeSearch(?Search $search): ?array\n {\n if ($search === null) {\n return null;\n }\n\n return [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ];\n }\n\n private function transformUser(int $userId): array\n {\n /* @var ?User $user */\n $user = $this->userRepository->find($userId);\n\n return [\n 'id' => $user?->getUuid(),\n 'name' => $user?->getName(),\n 'email' => $user?->getEmailAddress(),\n 'photoUrl' => $user?->getPhotoUrl(),\n ];\n }\n\n public function create(array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $validatedData['created_by'] = auth()->id();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function update(string $uuid, array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $report = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $report) {\n throw new InvalidArgumentException('Report not found');\n }\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Create an Ask Jiminny report.\n */\n public function createAskJiminnyReport(array $data, User $creator): array\n {\n $validatedData = $this->validateAskJiminnyReportData($data, $creator);\n $validatedData['created_by'] = $creator->getId();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Update an Ask Jiminny report.\n */\n public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array\n {\n if (! $report->isAskJiminnyReport()) {\n throw new InvalidArgumentException('Report is not an Ask Jiminny report');\n }\n\n $validatedData = $this->validateAskJiminnyReportData($data, $user);\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array\n {\n if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {\n throw new InvalidArgumentException(\n 'This report is missing a saved search or prompt. ' .\n 'Edit the report to complete the setup before enabling it.'\n );\n }\n\n $this->automatedReportsRepository->update($report, ['status' => $status]);\n\n return $this->transformReportFullView($report->fresh());\n }\n\n /**\n * Validate and transform data for Ask Jiminny reports.\n */\n private function validateAskJiminnyReportData(array $data, User $user): array\n {\n // Validate name\n $name = trim($data['report_name'] ?? '');\n if (empty($name)) {\n throw new InvalidArgumentException('Report name is required');\n }\n if (mb_strlen($name) > 50) {\n throw new InvalidArgumentException('Report name must be 50 characters or less');\n }\n\n // Validate frequency (only daily, weekly, monthly for Ask Jiminny)\n $frequency = $data['frequency'] ?? null;\n $askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];\n if (! in_array($frequency, $askJiminnyFrequencies, true)) {\n throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');\n }\n\n // Validate expiration date\n $expiresAt = $data['expires_on'] ?? null;\n if (empty($expiresAt)) {\n throw new InvalidArgumentException('Expiration date is required');\n }\n\n try {\n $expiresAtDate = Carbon::parse($expiresAt);\n } catch (InvalidFormatException $e) {\n throw new InvalidArgumentException('Expiration date format is invalid');\n }\n $maxExpiration = Carbon::now()->addYear()->endOfDay();\n if ($expiresAtDate->gt($maxExpiration)) {\n throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');\n }\n if ($expiresAtDate->isPast()) {\n throw new InvalidArgumentException('Expiration date cannot be in the past');\n }\n\n // Validate saved search\n $activitySearchId = $data['saved_search'] ?? null;\n if (empty($activitySearchId)) {\n throw new InvalidArgumentException('Saved search is required');\n }\n $savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);\n if (! $savedSearch) {\n throw new InvalidArgumentException('Saved search not found or does not belong to you');\n }\n\n // Validate saved prompt\n $askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;\n if (empty($askAnythingPromptId)) {\n throw new InvalidArgumentException('Ask Jiminny prompt is required');\n }\n $prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);\n if (! $prompt) {\n throw new InvalidArgumentException('Ask Jiminny prompt not found');\n }\n\n // Validate status\n $status = $data['enabled'] ?? false;\n\n $recipientUserIds = [$user->getId()];\n\n if (! empty($data['share_users'])) {\n $sharedUserIds = $this->validateAndGetUserIdsByTeam(\n $user->team,\n (array) $data['share_users']\n );\n $recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);\n }\n\n $sharedGroupIds = [];\n if (! empty($data['share_teams'])) {\n $sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);\n }\n\n $recipientUserIds = array_values(array_unique($recipientUserIds));\n\n return [\n 'team_id' => $user->getTeamId(),\n 'type' => self::TYPE_ASK_JIMINNY,\n 'status' => (bool) $status,\n 'frequency' => $frequency,\n 'custom_name' => $name,\n 'activity_search_id' => $savedSearch->getId(),\n 'ask_anything_prompt_id' => $prompt->getId(),\n 'expires_at' => $expiresAtDate->toDateString(),\n 'media_types' => [self::MEDIA_TYPE_PDF],\n 'call_types' => [],\n 'recipients' => ['users' => $recipientUserIds],\n 'groups' => $sharedGroupIds,\n ];\n }\n\n public static function getAskJiminnyFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::ASK_JIMINNY_FREQUENCIES);\n }\n\n public function getAskJiminnyReportFilters(User $user): array\n {\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n return [\n [\n 'id' => 'prompt',\n 'label' => 'Prompt',\n 'options' => $prompts,\n ],\n [\n 'id' => 'saved_search',\n 'label' => 'Saved Search',\n 'options' => $savedSearches,\n ],\n ];\n }\n\n public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array\n {\n $team = $user->getTeam();\n $userTimezone = $user->getTimezone();\n\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n $teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ])->values()->all();\n\n $shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];\n\n $sharedTeamsValue = [];\n $sharedUsersValue = [];\n if ($report) {\n $sharedTeamsValue = $this->transformGroups($team, $report->getGroups());\n\n $recipientUserIds = $report->getRecipients()['users'] ?? [];\n $creatorId = $report->getAttribute('created_by');\n $sharedUserIds = array_values(array_filter(\n $recipientUserIds,\n static fn ($id) => $id !== $creatorId\n ));\n $sharedUsersValue = collect($sharedUserIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (User $u) => [\n 'id' => $u->getUuid(),\n 'name' => $u->getName(),\n ])\n ->values()\n ->all();\n }\n\n return [\n 'fields' => [\n [\n 'id' => 'enabled',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'label' => '',\n 'value' => $report?->getStatus() ?? false,\n ],\n [\n 'id' => 'report_name',\n 'inputType' => InputTypeEnum::TEXT,\n 'label' => 'Name',\n 'placeholder' => 'Enter name',\n 'required' => true,\n 'validation' => ['maxLength' => 50],\n 'value' => $report?->getCustomName() ?? '',\n ],\n [\n 'id' => 'frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Frequency',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::ASK_JIMINNY_FREQUENCIES,\n 'value' => $report ? $this->transformFrequency($report->getFrequency()) : null,\n ],\n [\n 'id' => 'expires_on',\n 'inputType' => InputTypeEnum::DATE,\n 'label' => 'Expires on',\n 'required' => true,\n 'placeholder' => 'Select',\n 'validation' => [\n 'minDate' => now($userTimezone)->toDateString(),\n 'maxDate' => now($userTimezone)->addYear()->toDateString(),\n ],\n 'value' => $report?->getExpiresAt()?->toDateString(),\n ],\n [\n 'id' => 'share_teams',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team',\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $teamGroups,\n 'value' => $sharedTeamsValue,\n ],\n [\n 'id' => 'share_users',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team member',\n 'required' => false,\n 'placeholder' => 'Select',\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'options' => $shareUsers,\n 'value' => $sharedUsersValue,\n ],\n [\n 'id' => 'saved_search',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Saved search',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $savedSearches,\n 'value' => $report && $report->getSavedSearch() ? [\n 'id' => $report->getSavedSearch()->getUuid(),\n 'name' => $report->getSavedSearch()->getName(),\n ] : null,\n ],\n [\n 'id' => 'ask_jiminny_prompt',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Ask Jiminny prompt',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $prompts,\n 'value' => $report && $report->getAskAnythingPrompt() ? [\n 'id' => $report->getAskAnythingPrompt()->getUuid(),\n 'name' => $report->getAskAnythingPrompt()->getTitle(),\n ] : null,\n ],\n ],\n ];\n }\n\n private function updateResultNames(AutomatedReport $automatedReport): void\n {\n $results = $this->automatedReportsRepository->getResultsByReport($automatedReport);\n\n foreach ($results as $result) {\n $result->update(['name' => $this->getReportFileName($result)]);\n }\n }\n\n public function updateStatus(string $uuid, array $data): array\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $automatedReport->update([\n 'status' => $status,\n ]);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n private function generateOneOffReport(AutomatedReport $automatedReport): void\n {\n // the scheduler handles all the other frequency types\n if ($automatedReport->getStatus() === false || $automatedReport->getFrequency() !== self::FREQUENCY_ONE_OFF) {\n return;\n }\n\n $this->dispatcher->dispatch(new RequestGenerateReportJob($automatedReport->getUuid()));\n }\n\n public function getReport(string $uuid, ?Partner $partner = null): AutomatedReport\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n if ($partner !== null && ! $partner->isDefaultPartner() && $automatedReport->team->partner_id !== $partner->getId()) {\n throw new ModelNotFoundException('Report not found');\n }\n\n return $automatedReport;\n }\n\n public function get(string $uuid, ?Partner $partner = null): array\n {\n $automatedReport = $this->getReport($uuid, $partner);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function list(string $sortColumn = 'created_at', string $sortDirection = 'desc', ?Partner $partner = null): array\n {\n $results = [];\n $collection = $this->automatedReportsRepository->getAllStandardReports($sortColumn, $sortDirection, $partner);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function listAskJiminnyReports(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): array {\n $results = [];\n $collection = $this->automatedReportsRepository->getAskJiminnyReportsByUser($user, $sortColumn, $sortDirection);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function delete(string $uuid): void\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $automatedReport->delete();\n }\n\n public function createReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n return $this->automatedReportsRepository->createResult(\n array_merge(\n [\n 'report_id' => $automatedReport->getId(),\n 'status' => AutomatedReportResult::STATUS_DEFAULT,\n ],\n $data\n )\n );\n }\n\n public function getOrCreateReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n $existing = $this->automatedReportsRepository->findLatestSameDayDefaultOrFailedResult($automatedReport);\n\n if ($existing !== null) {\n $existing->update(['status' => AutomatedReportResult::STATUS_DEFAULT]);\n\n return $existing;\n }\n\n return $this->createReportResult($automatedReport, $data);\n }\n\n public function getReportResult(string $resultUuid): AutomatedReportResult\n {\n $report = $this->automatedReportsRepository->findResultByUuid($resultUuid);\n\n if (! $report) {\n throw new ModelNotFoundException('Report Result not found');\n }\n\n return $report;\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return $this->automatedReportsRepository->findChildResult($result, $type);\n }\n\n // prophet API calls\n /**\n * @throws ApplicationException\n */\n public function getGenerateReportPayload(AutomatedReport $automatedReport, string $reportResultUuid): array\n {\n $period = $this->calculateFromAndToDate($automatedReport);\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n return [\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResultUuid,\n 'report_type' => $automatedReport->getType(),\n 'media_types' => $automatedReport->getMediaTypes(),\n 'from_date' => $fromDate->startOfDay()->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->endOfDay()->format(DateTimeInterface::RFC3339),\n 'group_ids' => $automatedReport->getGroups(),\n 'call_deal_stage' => $automatedReport->getDealAtCallStages(),\n 'current_deal_stage' => $automatedReport->getCurrentDealStages(),\n 'deal_min_value' => $automatedReport->getDealValueMin(),\n 'deal_max_value' => $automatedReport->getDealValueMax(),\n 'call_types' => $automatedReport->getCallTypes(),\n 'call_duration_min_seconds' => $automatedReport->getCallDurationMin(),\n 'call_duration_max_seconds' => $automatedReport->getCallDurationMax(),\n 'special_requirements' => $automatedReport->getAdditionalPromptInput(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->formatReportPeriodName(\n $automatedReport->getFrequency(),\n $fromDate,\n $toDate,\n ),\n 'playbook_categories' => $automatedReport->getPlaybookCategories(),\n 'custom_name' => $automatedReport->getCustomName(),\n ];\n }\n\n // $inputPayload - FE payload structure\n public function getActivitiesCountPayload(array $inputPayload): array\n {\n // Use validateAndTransformData to validate and normalize input\n $validatedData = $this->validateAndTransformData($inputPayload);\n $period = $this->calculateFromAndToDatePeriod(\n $validatedData['frequency'],\n Carbon::parse($validatedData['from']),\n Carbon::parse($validatedData['to']),\n );\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n // Create payload similar to getGenerateReportPayload\n return [\n 'team_id' => $validatedData['team_id'],\n 'group_ids' => $validatedData['groups'] ?? [],\n 'report_type' => $validatedData['type'],\n 'from_date' => $fromDate->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->format(DateTimeInterface::RFC3339),\n 'call_deal_stage' => $validatedData['deal_at_call_stages'] ?? [],\n 'current_deal_stage' => $validatedData['current_deal_stages'] ?? [],\n 'deal_min_value' => $validatedData['deal_value_min'] ?? null,\n 'deal_max_value' => $validatedData['deal_value_max'] ?? null,\n 'call_types' => $validatedData['call_types'],\n 'call_duration_min_seconds' => $validatedData['call_duration_min'] ?? null,\n 'call_duration_max_seconds' => $validatedData['call_duration_max'] ?? null,\n 'special_requirements' => $validatedData['additional_prompt_input'] ?? null,\n 'playbook_categories' => $validatedData['playbook_categories'] ?? [],\n 'request_id' => null,\n 'callback_url' => null,\n ];\n }\n\n public function shouldSendReport(array $users, ?CarbonInterface $generatedAt = null): bool\n {\n if (empty($users)) {\n return false;\n }\n\n $earliestTz = collect($users)\n ->mapWithKeys(function (array $user) {\n $tz = new DateTimeZone($user['timezone']);\n $nowUtc = new DateTime('now', new DateTimeZone('UTC'));\n $offset = $tz->getOffset($nowUtc);\n\n return [$user['timezone'] => $offset];\n })\n ->sortDesc()\n ->keys()\n ->first();\n\n $now = Carbon::now($earliestTz);\n $isScheduledTime = (int) $now->format('H') === self::SENT_REPORT_AT_HOURS;\n\n if ($isScheduledTime) {\n return true;\n }\n\n return $this->hasPassedScheduledTime($generatedAt, $earliestTz);\n }\n\n public function hasPassedScheduledTime(?CarbonInterface $generatedAt, string $timezone): bool\n {\n if ($generatedAt === null) {\n return false;\n }\n\n $now = Carbon::now($timezone);\n $scheduledTime = $now->copy()->setTime(self::SENT_REPORT_AT_HOURS, 0, 0);\n\n if ($now->hour < self::SENT_REPORT_AT_HOURS) {\n $scheduledTime = $scheduledTime->subDay();\n }\n\n $scheduledTimeUtc = $scheduledTime->copy()->utc();\n $generatedAtUtc = $generatedAt->copy()->utc();\n $nowUtc = $now->copy()->utc();\n\n return $generatedAtUtc->lt($scheduledTimeUtc) && $nowUtc->gt($scheduledTimeUtc);\n }\n\n public function calculateFromAndToDatePeriod(\n string $frequency,\n ?Carbon $fromDate = null,\n ?Carbon $toDate = null,\n DateTimeZone|string|null $timezone = 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($timezone);\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()->subWeek()->startOfWeek(CarbonInterface::MONDAY),\n 'toDate' => $now->copy()->subWeek()->endOfWeek(CarbonInterface::SUNDAY),\n ],\n self::FREQUENCY_MONTHLY => [\n 'fromDate' => $now->copy()->subMonthNoOverflow()->startOfMonth(),\n 'toDate' => $now->copy()->subMonthNoOverflow()->endOfMonth(),\n ],\n self::FREQUENCY_QUARTERLY => [\n 'fromDate' => $now->copy()->subQuarterNoOverflow()->startOfQuarter(),\n 'toDate' => $now->copy()->subQuarterNoOverflow()->endOfQuarter(),\n ],\n default => throw new InvalidArgumentException(\"Unsupported frequency: {$frequency}\"),\n };\n }\n\n private function calculateFromAndToDate(AutomatedReport $automatedReport): array\n {\n return $this->calculateFromAndToDatePeriod(\n $automatedReport->getFrequency(),\n $automatedReport->getFrom(),\n $automatedReport->getTo()\n );\n }\n\n public function getAskJiminnyGenerateReportPayload(\n AutomatedReport $automatedReport,\n AutomatedReportResult $reportResult,\n array $activityIds,\n ): array {\n return [\n 'user_question' => $automatedReport->getAskAnythingPrompt()?->getContent(),\n 'call_ids' => array_map('strval', $activityIds),\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResult->getUuid(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->getReportPeriodName($reportResult),\n 'report_name' => $automatedReport->getCustomName(),\n ];\n }\n\n private function getCallbackUrl(): string\n {\n return $this->webhookService->route('jiminny.webhook.reports.ready');\n }\n\n /**\n * Validate and transform payload data for automated reports\n *\n * @param array $data\n *\n * @throws InvalidArgumentException\n *\n * @return array\n */\n private function validateAndTransformData(array $data): array\n {\n // Validate organization (team) and check feature\n $team = $this->validateOrganization($data['organization'] ?? null);\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $type = $this->validateReportType($data['report_type'] ?? null);\n $frequency = $this->validateFrequency($data['frequency'] ?? null);\n $additionalPromptInput = $this->validateAdditionalPromptInput(\n $data['additional_prompt_input'] ?? null\n );\n $customReportName = $this->validateCustomReportName($data['custom_name'] ?? null);\n\n // Prepare data for the database\n $reportData = [\n 'team_id' => $team->getId(),\n 'type' => $type,\n 'status' => $status,\n 'frequency' => $frequency,\n 'additional_prompt_input' => $additionalPromptInput,\n 'custom_name' => $customReportName,\n ];\n\n // Validate deal values\n $reportData = $this->validateDealValues($data, $reportData);\n\n // Validate date range\n $reportData = $this->validateDateRange($data, $reportData, $frequency);\n\n // Validate call durations\n $reportData = $this->validateCallDurations($data, $reportData);\n\n // Validate call types\n $reportData = $this->validateCallTypes($data, $reportData);\n\n // Validate media types\n $reportData = $this->validateMediaTypes($data, $reportData);\n\n // Validate groups\n if (isset($data['teams'])) {\n $reportData['groups'] = $this->validateAndGetGroupIds($team, $data['teams']);\n }\n\n // Validate deal stages\n $reportData = $this->validateDealStages($data, $reportData, $team, $type);\n\n // Validate playbook categories\n $reportData = $this->validatePlaybookCategories($data, $reportData, $team);\n\n // Validate recipients\n $reportData['recipients'] = [\n 'users' => $this->validateAndGetUserIdsByTeam($team, $data['recipients'] ?? []),\n ];\n\n if (isset($data['jiminny_recipients'])) {\n // Validate Jiminny recipients\n $reportData['jiminny_recipients'] = [\n 'users' => $this->validateAndGetJiminnyUserIds((array) $data['jiminny_recipients']),\n ];\n }\n\n return $reportData;\n }\n\n private function validateDealValues(array $data, array $reportData): array\n {\n if (isset($data['min_deal_value'])) {\n $reportData['deal_value_min'] = (int) $data['min_deal_value'];\n\n if ($reportData['deal_value_min'] > 4294967295 || $reportData['deal_value_min'] < 0) {\n throw new InvalidArgumentException('Min deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_deal_value'])) {\n $reportData['deal_value_max'] = (int) $data['max_deal_value'];\n\n if ($reportData['deal_value_max'] > 4294967295 || $reportData['deal_value_max'] < 0) {\n throw new InvalidArgumentException('Max deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['min_deal_value'], $data['max_deal_value'])\n && $data['min_deal_value'] > $data['max_deal_value']\n ) {\n throw new InvalidArgumentException('Min deal value cannot be greater than max deal value');\n }\n\n return $reportData;\n }\n\n private function validateDateRange(array $data, array $reportData, string $frequency): array\n {\n // Set date range only for one_off frequency\n if ($frequency === 'one_off') {\n if (isset($data['start_date_period'])) {\n $reportData['from'] = $this->parseDate($data['start_date_period']);\n }\n\n if (isset($data['end_date_period'])) {\n $reportData['to'] = $this->parseDate($data['end_date_period']);\n }\n\n if (empty($reportData['from']) || empty($reportData['to'])) {\n throw new InvalidArgumentException(\n 'Start date and end date are required for one_off frequency'\n );\n }\n } else {\n $reportData['from'] = null;\n $reportData['to'] = null;\n }\n\n return $reportData;\n }\n\n private function validateCallDurations(array $data, array $reportData): array\n {\n // Convert call durations from minutes to seconds\n if (isset($data['min_call_duration'])) {\n $reportData['call_duration_min'] = (int) $data['min_call_duration'] * 60;\n\n if ($reportData['call_duration_min'] > 4294967295 || $reportData['call_duration_min'] < 0) {\n throw new InvalidArgumentException('Min call duration should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_call_duration'])) {\n $reportData['call_duration_max'] = (int) $data['max_call_duration'] * 60;\n\n if ($reportData['call_duration_max'] > 4294967295 || $reportData['call_duration_max'] < 0) {\n throw new InvalidArgumentException('Max call duration should be between 0 and 4294967295');\n }\n }\n\n return $reportData;\n }\n\n private function validateCallTypes(array $data, array $reportData): array\n {\n // Set call types\n $reportData['call_types'] = $data['call_type'] ?? [];\n if (empty($reportData['call_types'])) {\n $reportData['call_types'] = self::getCallTypes();\n }\n\n foreach ($reportData['call_types'] as $callType) {\n if (! in_array($callType, self::getCallTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Call type %s is invalid', $callType));\n }\n }\n\n return $reportData;\n }\n\n private function validateMediaTypes(array $data, array $reportData): array\n {\n // Set media types from input data\n $reportData['media_types'] = $data['media_types'] ?? [];\n\n if (empty($reportData['media_types'])) {\n throw new InvalidArgumentException('Media types are required');\n }\n\n foreach ($reportData['media_types'] as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n throw new InvalidArgumentException(sprintf('Media type %s is invalid', $mediaType));\n }\n }\n\n return $reportData;\n }\n\n private function validateDealStages(array $data, array $reportData, Team $team, string $reportType): array\n {\n // Validate and set deal stages\n if (isset($data['deal_stage_at_call'])) {\n $reportData['deal_at_call_stages'] =\n $this->validateAndGetDealStageIds($team, $data['deal_stage_at_call'], 'Deal stage at call');\n }\n\n if (isset($data['current_deal_stage'])) {\n $reportData['current_deal_stages'] =\n $this->validateAndGetDealStageIds($team, $data['current_deal_stage'], 'Current deal stage');\n }\n\n // Ensure current_deal_stage is not provided for loss_analysis report type\n if ($reportType === self::TYPE_LOSS_ANALYSIS && ! empty($data['current_deal_stage'])) {\n throw new InvalidArgumentException('Current deal stage is not applicable for Loss Analysis reports');\n }\n\n return $reportData;\n }\n\n // transform uuid to id\n private function validatePlaybookCategories(array $data, array $reportData, Team $team): array\n {\n $key = 'playbook_categories';\n\n if (isset($data[$key])) {\n $payloadIds = $data[$key];\n $ids = [];\n\n foreach ($payloadIds as $uuid) {\n $uuid = (string) $uuid;\n\n try {\n $playbookCategory = $this->playbookCategoryRepository->findByUuid($uuid);\n } catch (Throwable $throwable) {\n Log::error(__METHOD__ . ' ' . $throwable->getMessage());\n\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory) {\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory->hasPlaybook()) {\n throw new InvalidArgumentException(sprintf('Playbook category %s has no playbook', $uuid));\n }\n\n if ($playbookCategory->getPlaybook()->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Playbook category %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $ids[] = $playbookCategory->getId();\n }\n\n $reportData[$key] = $ids;\n }\n\n return $reportData;\n }\n\n private function validateReportStatus($status): bool\n {\n if (! in_array($status, [true, false], true)) {\n throw new InvalidArgumentException('Report status is invalid');\n }\n\n return $status;\n }\n\n private function validateReportType($type): string\n {\n if (! in_array($type, self::getTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Report type is invalid: %s', $type));\n }\n\n return $type;\n }\n\n private function validateFrequency($frequency): string\n {\n if (! in_array($frequency, self::getFrequencies(), true)) {\n throw new InvalidArgumentException('Frequency is invalid');\n }\n\n return $frequency;\n }\n\n private function validateAdditionalPromptInput(?string $additionalPromptInput): ?string\n {\n if ($additionalPromptInput && strlen($additionalPromptInput) > 5000) {\n throw new InvalidArgumentException('Additional Prompt Input should be less than 5000 characters');\n }\n\n return $additionalPromptInput;\n }\n\n private function validateCustomReportName(?string $customReportName): ?string\n {\n if ($customReportName === null || $customReportName === '') {\n return null;\n }\n\n if (strlen($customReportName) > 70) {\n throw new InvalidArgumentException('Custom report name should be less than 70 characters');\n }\n\n return $customReportName;\n }\n\n private function validateOrganization(?string $organizationUuid): Team\n {\n if (! $organizationUuid) {\n throw new InvalidArgumentException('Organization is required');\n }\n\n $team = $this->teamRepository->idOrUuid($organizationUuid);\n\n if (! $team) {\n throw new InvalidArgumentException('Organization not found');\n }\n\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n throw new InvalidArgumentException('Organization does not have the Automated Reports feature');\n }\n\n return $team;\n }\n\n private function validateAndGetGroupIds(Team $team, array $teamUuids): array\n {\n $groupIds = [];\n\n foreach ($teamUuids as $uuid) {\n $group = $this->groupRepository->findByUuid($uuid);\n\n if ($group === null || $group->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Group %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $groupIds[] = $group->getId();\n\n }\n\n return $groupIds;\n }\n\n private function validateAndGetDealStageIds(Team $team, array $stageUuids, string $propertyLabel): array\n {\n $stageIds = [];\n\n foreach ($stageUuids as $uuid) {\n $stage = $this->stageRepository->findByUuid($uuid);\n\n if ($stage === null || $stage->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Stage %s not found for team %s for %s', $uuid, $team->getUuid(), $propertyLabel)\n );\n }\n\n $stageIds[] = $stage->getId();\n }\n\n return $stageIds;\n }\n\n private function validateAndGetUserIds(array $userUuids, callable $teamCheck): array\n {\n if (empty($userUuids)) {\n return [];\n }\n\n $userIds = [];\n\n foreach ($userUuids as $uuid) {\n $user = $this->userRepository->findByUuid($uuid);\n\n if (! $user || ! $user->isStatusActive()) {\n throw new InvalidArgumentException(\n sprintf('User %s not found or is not active', $uuid)\n );\n }\n\n if (! $teamCheck($user)) {\n throw new InvalidArgumentException(\n sprintf('User %s does not belong to the allowed team(s)', $uuid)\n );\n }\n\n $userIds[] = $user->getId();\n }\n\n return $userIds;\n }\n\n private function validateAndGetUserIdsByTeam(Team $team, array $userUuids): array\n {\n return $this->validateAndGetUserIds($userUuids, fn ($user) => $user->getTeamId() === $team->getId());\n }\n\n private function validateAndGetJiminnyUserIds(array $userUuids): array\n {\n $allowedTeamIds = config('kiosk.teamIds', []);\n\n return $this->validateAndGetUserIds($userUuids, fn ($user) => in_array($user->getTeamId(), $allowedTeamIds, true));\n }\n\n private function parseDate(string $dateString): string\n {\n return date('Y-m-d H:i:s', strtotime($dateString));\n }\n\n private function generateReportResultViewUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.view', ['uuid' => $result->getUuid()]);\n }\n\n private function generateReportResultDownloadUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.download', ['uuid' => $result->getUuid()]);\n }\n\n private function getReportMediaRouteResource(AutomatedReportResult $result): string\n {\n if ($result->getMediaType() === self::MEDIA_TYPE_PDF) {\n return self::PDF_KEY;\n } elseif ($result->getMediaType() === self::MEDIA_TYPE_PODCAST) {\n return self::AUDIO_KEY;\n }\n\n throw new \\InvalidArgumentException('Unknown media type.');\n }\n\n public function getMediaPath(AutomatedReportResult $result): ?string\n {\n $url = match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => $result->getPdfUrl(),\n self::MEDIA_TYPE_PODCAST => $result->getPodcastAudioUrl(),\n default => null,\n };\n\n if ($url === null) {\n return null;\n }\n\n $path = parse_url(trim($url, '\"\\''), PHP_URL_PATH);\n\n return $path ?: null;\n }\n\n public function getFilenameSuffix(AutomatedReportResult $result): ?string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => 'Podcast',\n default => null,\n };\n }\n\n public function getMailSubjectSuffix(AutomatedReportResult $result): string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => 'report',\n self::MEDIA_TYPE_PODCAST => 'podcast',\n default => '',\n };\n }\n\n public function getMediaTypeMetadata(AutomatedReportResult $result): array\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => ['extension' => 'mp3', 'mime' => 'audio/mpeg'],\n self::MEDIA_TYPE_PDF => ['extension' => 'pdf', 'mime' => 'application/pdf'],\n default => ['extension' => null, 'mime' => null],\n };\n }\n\n public function deleteS3Files(AutomatedReportResult $result): void\n {\n $teamUuid = $result->getReport()->getTeam()->getUuid();\n $reportUuid = $result->getUuid();\n\n // delete all files for a report uuid no mather of pdf, podcast, or both\n // in case of both - the podcast files are linked to the pdf (parent) uuid\n // pdf and podcast date times should be close\n $path = sprintf('%s/%s/%s', $teamUuid, self::S3_DIR, $reportUuid);\n\n foreach (self::FILE_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted S3 file', [\n 'path' => $file,\n ]);\n }\n }\n\n foreach (self::FILE_PODCAST_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '_podcast.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted Podcast S3 file', [\n 'path' => $file,\n ]);\n }\n }\n }\n\n /**\n *\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): Collection\n {\n return $this->automatedReportsRepository->getTeamIdsWithReportsResults($teamId);\n }\n\n /**\n * Core delete logic for report results using a query\n *\n * @param Builder $query\n * @param array $logContext\n *\n * @return int\n */\n private function deleteReportResultsByQuery(Builder $query, array $logContext = []): int\n {\n $deletedCount = 0;\n\n if ($query->exists()) {\n Log::info(\n 'Run delete report results',\n array_merge(\n $logContext,\n [\n 'service' => 'AutomatedReportsService',\n ]\n )\n );\n\n $query->chunkById(50, function ($results) use (&$deletedCount, $logContext) {\n foreach ($results as $result) {\n $this->deleteReportResult($result);\n $deletedCount++;\n\n Log::info(\n 'Deleted a report result',\n array_merge(\n $logContext,\n [\n 'result_id' => $result->getId(),\n 'report_id' => $result->getReportId(),\n ]\n )\n );\n }\n });\n }\n\n return $deletedCount;\n }\n\n /**\n * Delete report results for a team by retention period\n *\n * @param Team $team\n * @param CarbonImmutable $retentionDate\n *\n * @return int Number of deleted report results\n */\n public function deleteReportsResultsInRetentionPeriod(Team $team, CarbonImmutable $retentionDate): int\n {\n $reportIds = $this->automatedReportsRepository->getReportIdsByTeam($team);\n\n if ($reportIds->isEmpty()) {\n return 0;\n }\n\n $query = $this->automatedReportsRepository\n ->getReportResultsQueryForRetention($team, $retentionDate);\n\n return $this->deleteReportResultsByQuery($query, [\n 'team_id' => $team->getId(),\n 'retention_date' => $retentionDate->toDateTimeString(),\n ]);\n }\n\n /**\n * Delete ALL report results for a specific automated report\n *\n * @param string $uuid\n *\n * @return int\n */\n public function deleteReportResults(string $uuid): int\n {\n $report = $this->getReport($uuid);\n\n $query = $this->automatedReportsRepository->getResultsByReportQuery($report);\n\n return $this->deleteReportResultsByQuery($query, [\n 'report_uuid' => $uuid,\n 'report_id' => $report->getId(),\n ]);\n }\n\n public function deleteReportResult(AutomatedReportResult $result): void\n {\n $this->deleteS3Files($result);\n\n $result->delete();\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getTeamReports(Team $team): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getReportsByTeam($team);\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getReportResults(AutomatedReport $report): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getResultsByReport($report);\n }\n\n public function deleteAllReportResults(AutomatedReport $report): void\n {\n $results = $this->getReportResults($report);\n\n /** @var AutomatedReportResult $result */\n foreach ($results as $result) {\n Log::info('Deleting result', [\n 'report' => $report->getId(),\n 'result' => $result->getId(),\n ]);\n\n $this->deleteReportResult($result);\n }\n }\n\n public function deleteAllData(Team $team): void\n {\n Log::info('Deleting automated report and results for team', [\n 'team' => $team->getId(),\n ]);\n\n $reports = $this->getTeamReports($team);\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n Log::info('Deleting report', [\n 'team' => $team->getId(),\n 'report' => $report->getId(),\n ]);\n\n $this->deleteAllReportResults($report);\n\n $report->delete();\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"21","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"18","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"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 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 * 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 `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\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;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","depth":4,"on_screen":true,"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 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 * 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 `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\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;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1751808696220340334
|
1126710648141684156
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskAnythingPromptServiceTest
Run 'AskAnythingPromptServiceTest'
Debug 'AskAnythingPromptServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
100
3
34
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Kiosk\AutomatedReports;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Carbon\Exceptions\InvalidFormatException;
use DateTime;
use DateTimeInterface;
use DateTimeZone;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\ActivitySearch\FilterDefinition\InputTypeEnum;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Partner;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Throwable;
class AutomatedReportsService
{
public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';
public const string TYPE_ASK_JIMINNY = 'ask_jiminny';
/**
* Standard report types (used by kiosk for existing automated reports).
*/
// @TODO this will add filter, however if we need to control feature by FF we need conditional logic
public const array TYPES = [
['id' => 'exec_summary', 'name' => 'Exec Summary'],
['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],
['id' => 'product_feedback', 'name' => 'Product Feedback'],
['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],
// ['id' => 'questions', 'name' => 'Questions'],
// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],
];
public const array ALL_TYPES = [
...self::TYPES,
['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],
];
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';
/**
* Frequencies for standard (non-Ask Jiminny) reports.
*/
public const array FREQUENCIES = [
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
/**
* Frequencies for Ask Jiminny reports.
*/
public const array ASK_JIMINNY_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
];
public const string MEDIA_TYPE_PDF = 'pdf';
public const string MEDIA_TYPE_PODCAST = 'podcast';
public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];
public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];
public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];
public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];
public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];
public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];
public const int SENT_REPORT_AT_HOURS = 5;
public const string PDF_KEY = 'pdf';
public const string AUDIO_KEY = 'audio';
private const array ALL_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
private const string S3_DIR = 'reports';
private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];
private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly GroupRepository $groupRepository,
private readonly UserRepository $userRepository,
private readonly StageRepository $stageRepository,
private readonly DealStagesService $dealStagesService,
private readonly RecipientsService $recipientsService,
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly Webhook $webhookService,
private readonly BusDispatcher $dispatcher,
private readonly ActivityTypeService $activityTypeService,
private readonly PlaybookCategoryRepository $playbookCategoryRepository,
private readonly AskAnythingPromptService $askAnythingPromptService,
private readonly SearchRepository $activitySearchRepository,
private readonly AskAnythingRepository $askAnythingRepository,
) {
}
public static function getTypes(): array
{
$types = self::TYPES;
return array_map(static function ($type) {
return $type['id'];
}, $types);
}
public static function getCallTypes(): array
{
return array_map(static function ($callType) {
return $callType['id'];
}, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);
}
public static function getFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::FREQUENCIES);
}
// front-facing structure
public function getReportEnabledFieldData(bool $value = false): array
{
return [
'id' => 'report_enabled',
'label' => '',
'inputType' => InputTypeEnum::TOGGLE,
'value' => $value,
];
}
// Organizations = Teams
public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array
{
$options = $this->getTeams(partner: $partner);
if ($shortVersion) {
return [
'id' => 'organization',
'label' => 'Organization',
'options' => $options,
];
}
return [
'id' => 'organization',
'label' => 'Organization',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $options,
'value' => $value,
'dependencies' => [
'teams',
'deal_stage_at_call',
'current_deal_stage',
'recipients',
ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,
],
'dependsOn' => [],
];
}
// Teams = Groups
public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array
{
if ($shortVersion) {
return [
'id' => 'teams',
'label' => 'Team',
'options' => $options,
];
}
return [
'id' => 'teams',
'label' => 'Team',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => false,
'placeholder' => 'Select',
'options' => $options,
'value' => $value, // value should be an array of objects {id, name}
'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],
'dependsOn' => [],
];
}
public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array
{
$types = [];
if ($team instanceof Team) {
if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
$types = self::TYPES;
}
if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {
$types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];
}
} else {
$types = self::TYPES;
}
if ($shortVersion) {
return [
'id' => 'report_type',
'label' => 'Report Type',
'options' => $types,
];
}
return [
'id' => 'report_type',
'label' => 'Report Type',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $types,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getFrequencyFieldData(?string $value = null): array
{
return [
'id' => 'frequency',
'label' => 'Frequency',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => self::FREQUENCIES,
'value' => $value,
'dependencies' => ['period'],
'dependsOn' => [],
];
}
public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array
{
return [
'id' => 'period',
'label' => 'Select one-off period',
'inputType' => InputTypeEnum::DATE_RANGE,
'required' => true,
'placeholder' => 'Select',
'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],
'queryParams' => [
'startDate' => 'start_date_period',
'endDate' => 'end_date_period',
],
'dependencies' => [],
'dependsOn' => ['frequency'],
];
}
public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array
{
return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);
}
public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);
}
public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);
}
public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'deal_value',
'label' => 'Deal Value',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_deal_value',
'max' => 'max_deal_value',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array
{
$value = [];
$conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;
$dialerOn && $value[] = self::CALL_TYPE_DIALER;
return [
'id' => 'call_type',
'label' => 'Call Type',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => [
self::CALL_TYPE_CONFERENCE,
self::CALL_TYPE_DIALER,
],
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getMediaTypeFieldData(?AutomatedReport $report = null): array
{
$value = [];
if ($report) {
$value = $this->transformMediaTypes($report);
}
return [
'id' => 'media_types',
'label' => 'Export as',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => self::MEDIA_TYPE_OBJECTS,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'call_duration',
'label' => 'Call Duration',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_call_duration',
'max' => 'max_call_duration',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getRecipientsFieldData(?Team $team = null, array $value = []): array
{
return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);
}
public function getJiminnyRecipientsFieldData(array $value = []): array
{
return $this->recipientsService->getJiminnyRecipientsFieldData($value);
}
public function getAdditionalPromptInputFieldData(?string $value = null): array
{
return [
'id' => 'additional_prompt_input',
'label' => 'Special requirements',
'inputType' => InputTypeEnum::TEXTAREA,
'required' => false,
'placeholder' => 'What should be the focus of the report?',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCustomReportNameFieldData(?string $value = null): array
{
return [
'id' => 'custom_name',
'label' => 'Custom report name',
'inputType' => InputTypeEnum::TEXT,
'required' => false,
'placeholder' => 'Enter custom name',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
// data providers
public function getTeams(?Partner $partner = null): array
{
$teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);
$teamData = [];
foreach ($teams as $team) {
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
continue;
}
$teamData[] = $this->transformTeam($team);
}
return $teamData;
}
public function getTeamGroups(string $teamUuid): array
{
$data = [];
$team = $this->getTeam($teamUuid);
if ($team !== null) {
$groups = $team->groups()->get();
foreach ($groups as $group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
];
}
}
return $data;
}
public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array
{
$data = [];
$teams = $this->getTeams(partner: $partner);
foreach ($teams as $team) {
if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {
continue;
}
$data[] = [
'label' => $team['name'],
'groups' => $this->getTeamGroups($team['id']),
];
}
return $data;
}
public function getTeam(string $teamUuid): ?Team
{
return $this->teamRepository->idOrUuid($teamUuid);
}
public function getTeamById(int $teamId): ?Team
{
return $this->teamRepository->find($teamId);
}
public function getGroupsUuids(AutomatedReport $report): array
{
$uuids = [];
$reportGroups = $report->getGroups();
foreach ($reportGroups as $groupId) {
if ($group = $this->groupRepository->find($groupId)) {
$uuids[] = $group->getUuid();
}
}
return $uuids;
}
public function getPlaybookCategoriesUuids(AutomatedReport $report): array
{
$uuids = [];
$playbookCategories = $report->getPlaybookCategories();
foreach ($playbookCategories as $id) {
if ($category = $this->playbookCategoryRepository->find($id)) {
$uuids[] = $category->getUuid();
}
}
return $uuids;
}
public function getDealAtCallStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getDealAtCallStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getCurrentDealStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getCurrentDealStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getRecipients());
}
public function getJiminnyUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function extractUserUuids(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => $user->getUuid())
->values()
->all();
}
// get mail data
public function getRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getRecipients());
}
/**
* @return array<UserContract>
*/
public function getRecipientUserObjects(AutomatedReport $report): array
{
$userIds = $report->getRecipients()['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->values()
->all();
}
private function getJiminnyRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function buildRecipientUsers(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => [
'email' => $user->getEmailAddress(),
'name' => $user->getName(),
'timezone' => $user->getTimezone()->getName(),
])
->values()
->all();
}
public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array
{
if ($report->isAskJiminnyReport()) {
$recipients = $this->resolveAskJiminnyRecipients($report);
} else {
$recipients = $this->getRecipientUsers($report);
if ($includeJiminny) {
$recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));
}
}
$emails = [];
return array_values(array_filter(
$recipients,
static function ($recipient) use (&$emails) {
if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {
return false;
}
$emails[] = $recipient['email'];
return true;
}
));
}
private function resolveAskJiminnyRecipients(AutomatedReport $report): array
{
$recipients = [];
$creator = $report->getCreator();
if ($creator !== null) {
$recipients[] = [
'email' => $creator->getEmailAddress(),
'name' => $creator->getName(),
'timezone' => $creator->getTimezone()->getName(),
];
}
return array_merge(
$recipients,
$this->buildRecipientUsers($report->getRecipients()),
$this->getGroupRecipientUsers($report),
);
}
private function getGroupRecipientUsers(AutomatedReport $report): array
{
$users = [];
foreach ($report->getGroups() as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group === null) {
continue;
}
foreach ($group->getMembers() as $member) {
$users[] = [
'email' => $member->getEmailAddress(),
'name' => $member->getName(),
'timezone' => $member->getTimezone()->getName(),
];
}
}
return $users;
}
public function getReportTypeName(AutomatedReportResult $report): string
{
$type = $report->getReport()->getType();
$getType = $this->transformReportType($type);
return $getType['name'];
}
public function getReportPeriodName(AutomatedReportResult $report): string
{
$from = $report->getFromDate();
$to = $report->getToDate();
$frequency = $report->getReport()->getFrequency();
if ($from === null || $to === null) {
if (! $report->getReport()->isAskJiminnyReport()) {
$invalidPeriod = $from === null ? 'from' : 'to';
throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);
}
$timezone = $report->getReport()->getCreator()?->getTimezone();
$period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);
$from = $period['fromDate'];
$to = $period['toDate'];
}
return $this->formatReportPeriodName($frequency, $from, $to);
}
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 getReportTeamsName(AutomatedReportResult $report): string
{
$groups = $report->getGroups();
if (empty($groups)) {
return 'All';
}
// Get group names from repository
$groupNames = [];
foreach ($groups as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group) {
$groupNames[] = $group->getName();
}
}
if (count($groupNames) === 1) {
// Single team format
$teamsName = $groupNames[0];
} else {
// Multiple teams format
$teamsName = implode(', ', $groupNames);
}
return $teamsName;
}
public function getReportFileName(AutomatedReportResult $report): string
{
$customName = $report->getReport()->getCustomName();
$periodName = $this->getReportPeriodName($report);
$filenameSuffix = $this->getFilenameSuffix($report);
if ($customName) {
if ($filenameSuffix) {
$customName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$customName} - {$periodName}");
}
$baseName = $this->getReportTypeName($report);
if ($filenameSuffix) {
$baseName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}");
}
public function getReportFileNameWithExtension(AutomatedReportResult $result): string
{
$extension = $this->getMediaTypeMetadata($result)['extension'];
return $this->getReportFileName($result) . '.' . $extension;
}
public function sanitizeFileName(string $fileName): string
{
return str_replace(['/', '\\'], '-', $fileName);
}
public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool
{
$recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);
if (in_array($user->getId(), $recipientIds, true)) {
return true;
}
if ($report->isAskJiminnyReport()) {
$groupId = $user->getGroupId();
if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {
return true;
}
}
return false;
}
public function transformReportResults(Collection $automatedReportResults): array
{
$data = [];
foreach ($automatedReportResults as $automatedReportResult) {
/** @var AutomatedReportResult $automatedReportResult */
$report = $automatedReportResult->getReport();
$createdBy = $report->getCreator();
$creator = [
'id' => $createdBy?->getUuid(),
'name' => $createdBy?->getName(),
'email' => $createdBy?->getEmailAddress(),
'photoUrl' => $createdBy?->getPhotoUrl(),
];
$data[] = [
'id' => $automatedReportResult->getUuid(),
'name' => $automatedReportResult->getName(),
'frequency' => $this->transformFrequency($report->getFrequency()),
'recipients' => $this->buildRecipients($report),
'report_type' => $this->transformReportType($report->getType()),
'media_type' => $automatedReportResult->getMediaType(),
'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),
'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),
'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),
'creator' => $creator,
];
}
return $data;
}
private function buildRecipients(AutomatedReport $report): array
{
$creatorUuid = $report->getCreator()?->getUuid();
$recipients = array_values(array_filter(
$this->transformRecipients($report->getRecipients()),
static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,
));
if (! $report->isAskJiminnyReport()) {
return $recipients;
}
return [
...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),
...$recipients,
];
}
public function hasCallTypeConference(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);
}
public function hasCallTypeDialer(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);
}
// transformers
private function transformTeam(Team $team): array
{
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
return [];
}
return [
'id' => $team->getUuid(),
'name' => $team->getName(),
];
}
private function transformReportFullView(AutomatedReport $report): array
{
$base = $this->transformReportBase($report);
return $report->getType() === self::TYPE_ASK_JIMINNY
? $base + $this->transformAskJiminnyFields($report)
: $base + $this->transformStandardReportFields($report);
}
private function transformReportBase(AutomatedReport $report): array
{
return [
'id' => $report->getUuid(),
'organization' => $this->transformOrganization(team: $report->getTeam()),
'report_type' => $this->transformReportType($report->getType()),
'frequency' => $this->transformFrequency($report->getFrequency()),
];
}
private function transformStandardReportFields(AutomatedReport $report): array
{
$team = $report->getTeam();
return [
'report_enabled' => $report->getStatus(),
'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),
'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),
'deal_value_min' => $report->getDealValueMin(),
'deal_value_max' => $report->getDealValueMax(),
'call_types' => $this->transformCallType($report->getCallTypes()),
'media_types' => $this->transformMediaTypes($report),
'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),
'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),
'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),
'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),
'recipients' => $this->transformRecipients($report->getRecipients()),
'created_by' => $this->transformCreator($report->getCreator()),
'additional_prompt_input' => $report->getAdditionalPromptInput(),
'custom_name' => $report->getCustomName(),
'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),
'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),
'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),
];
}
private function transformAskJiminnyFields(AutomatedReport $report): array
{
$team = $report->getTeam();
$creatorId = $report->getAttribute('created_by');
$explicitUserIds = array_values(array_filter(
$report->getRecipients()['users'] ?? [],
static fn ($id) => $id !== $creatorId
));
return [
'report_name' => $report->getCustomName(),
'enabled' => $report->getStatus(),
'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),
'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),
'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),
'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),
];
}
private function transformOrganization(?Team $team): array
{
return [
'id' => $team?->getUuid(),
'name' => $team?->getName(),
];
}
private function transformReportType(string $type): array
{
foreach (self::ALL_TYPES as $typeItem) {
if ($typeItem['id'] === $type) {
return $typeItem;
}
}
return [];
}
private function transformCallType(array $types): array
{
$result = [];
$callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];
foreach ($types as $type) {
foreach ($callTypes as $callTypeItem) {
if ($callTypeItem['id'] === $type) {
$result[] = $callTypeItem;
break;
}
}
}
return $result;
}
private function transformMediaTypes(AutomatedReport $report): array
{
$values = [];
foreach ($report->getMediaTypes() as $mediaType) {
if (! in_array($mediaType, self::MEDIA_TYPES, true)) {
continue;
}
$values[] = match ($mediaType) {
self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,
self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,
};
}
return $values;
}
private function transformFrequency(string $frequency): array
{
foreach (self::ALL_FREQUENCIES as $frequencyItem) {
if ($frequencyItem['id'] === $frequency) {
return $frequencyItem;
}
}
return [];
}
public function transformDurationToMinutes(?int $duration): ?int
{
if (! $duration) {
return null;
}
return (int) ($duration / 60);
}
private function transformGroups(?Team $team, array $groupsIds): array
{
if (empty($groupsIds) || ! $team) {
return [];
}
$data = [];
foreach ($groupsIds as $groupId) {
$group = $team->groups()->where('id', $groupId)->first();
if ($group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
'photoUrl' => $group->getPhotoUrl(),
];
}
}
return $data;
}
private function transformStages(?Team $team, array $stagesIds): array
{
if (empty($stagesIds) || ! $team) {
return [];
}
$data = [];
foreach ($stagesIds as $stageId) {
$stage = $team->stages()->where('id', $stageId)->first();
if ($stage) {
$data[] = [
'id' => $stage->getUuid(),
'name' => $stage->getName(),
];
}
}
return $data;
}
private function transformRecipients(array $recipients): array
{
$users = [];
foreach ($recipients['users'] ?? [] as $userId) {
$users[] = $this->transformUser($userId);
}
return $users;
}
private function transformCreator(?User $user): ?array
{
if ($user === null) {
return null;
}
return $this->transformUser($user->getId());
}
private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array
{
if ($prompt === null) {
return null;
}
return [
'id' => $prompt->getUuid(),
'name' => $prompt->getTitle(),
];
}
private function transformSafeSearch(?Search $search): ?array
{
if ($search === null) {
return null;
}
return [
'id' => $search->getUuid(),
'name' => $search->getName(),
];
}
private function transformUser(int $userId): array
{
/* @var ?User $user */
$user = $this->userRepository->find($userId);
return [
'id' => $user?->getUuid(),
'name' => $user?->getName(),
'email' => $user?->getEmailAddress(),
'photoUrl' => $user?->getPhotoUrl(),
];
}
public function create(array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$validatedData['created_by'] = auth()->id();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
public function update(string $uuid, array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$report = $this->automatedReportsRepository->findByUuid($uuid);
if (! $report) {
throw new InvalidArgumentException('Report not found');
}
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
/**
* Create an Ask Jiminny report.
*/
public function createAskJiminnyReport(array $data, User $creator): array
{
$validatedData = $this->validateAskJiminnyReportData($data, $creator);
$validatedData['created_by'] = $creator->getId();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
return $this->transformReportFullView($automatedReport);
}
/**
* Update an Ask Jiminny report.
*/
public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array
{
if (! $report->isAskJiminnyReport()) {
throw new InvalidArgumentException('Report is not an Ask Jiminny report');
}
$validatedData = $this->validateAskJiminnyReportData($data, $user);
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
return $this->transformReportFullView($automatedReport);
}
public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array
{
if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {
throw new InvalidArgumentException(
'This report is missing a saved search or prompt. ' .
'Edit the report to complete the setup before enabling it.'
);
}
$this->automatedReportsRepository->update($report, ['status' => $status]);
return $this->transformReportFullView($report->fresh());
}
/**
* Validate and transform data for Ask Jiminny reports.
*/
private function validateAskJiminnyReportData(array $data, User $user): array
{
// Validate name
$name = trim($data['report_name'] ?? '');
if (empty($name)) {
throw new InvalidArgumentException('Report name is required');
}
if (mb_strlen($name) > 50) {
throw new InvalidArgumentException('Report name must be 50 characters or less');
}
// Validate frequency (only daily, weekly, monthly for Ask Jiminny)
$frequency = $data['frequency'] ?? null;
$askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];
if (! in_array($frequency, $askJiminnyFrequencies, true)) {
throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');
}
// Validate expiration date
$expiresAt = $data['expires_on'] ?? null;
if (empty($expiresAt)) {
throw new InvalidArgumentException('Expiration date is required');
}
try {
$expiresAtDate = Carbon::parse($expiresAt);
} catch (InvalidFormatException $e) {
throw new InvalidArgumentException('Expiration date format is invalid');
}
$maxExpiration = Carbon::now()->addYear()->endOfDay();
if ($expiresAtDate->gt($maxExpiration)) {
throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');
}
if ($expiresAtDate->isPast()) {
throw new InvalidArgumentException('Expiration date cannot be in the past');
}
// Validate saved search
$activitySearchId = $data['saved_search'] ?? null;
if (empty($activitySearchId)) {
throw new InvalidArgumentException('Saved search is required');
}
$savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);
if (! $savedSearch) {
throw new InvalidArgumentException('Saved search not found or does not belong to you');
}
// Validate saved prompt
$askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;
if (empty($askAnythingPromptId)) {
throw new InvalidArgumentException('Ask Jiminny prompt is required');
}
$prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);
if (! $prompt) {
throw new InvalidArgumentException('Ask Jiminny prompt not found');
}
// Validate status
$status = $data['enabled'] ?? false;
$recipientUserIds = [$user->getId()];
if (! empty($data['share_users'])) {
$sharedUserIds = $this->validateAndGetUserIdsByTeam(
$user->team,
(array) $data['share_users']
);
$recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);
}
$sharedGroupIds = [];
if (! empty($data['share_teams'])) {
$sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);
}
$recipientUserIds = array_values(array_unique($recipientUserIds));
return [
'team_id' => $user->getTeamId(),
'type' => self::TYPE_ASK_JIMINNY,
'status' => (bool) $status,
'frequency' => $frequency,
'custom_name' => $name,
'activity_search_id' => $savedSearch->getId(),
'ask_anything_prompt_id' => $prompt->getId(),
'expires_at' => $expiresAtDate->toDateString(),
'media_types' => [self::MEDIA_TYPE_PDF],
'call_types' => [],
'recipients' => ['users' => $recipientUserIds],
'groups' => $sharedGroupIds,
];
}
public static function getAskJiminnyFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::ASK_JIMINNY_FREQUENCIES);
}
public function getAskJiminnyReportFilters(User $user): array
{
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
return [
[
'id' => 'prompt',
'label' => 'Prompt',
'options' => $prompts,
],
[
'id' => 'saved_search',
'label' => 'Saved Search',
'options' => $savedSearches,
],
];
}
public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array
{
$team = $user->getTeam();
$userTimezone = $user->getTimezone();
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
$teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [
'id' => $group->getUuid(),
'name' => $group->getName(),
])->values()->all();
$shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];
$sharedTeamsValue = [];
$sharedUsersValue = [];
if ($report) {
$sharedTeamsValue = $this->transformGroups($team, $report->getGroups());
$recipientUserIds = $report->getRecipients()['users'] ?? [];
$creatorId = $report->getAttribute('created_by');
$sharedUserIds = array_values(array_filter(
$recipientUserIds,
static fn ($id) => $id !== $creatorId
));
$sharedUsersValue = collect($sharedUserIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (User $u) => [
'id' => $u->getUuid(),
'name' => $u->getName(),
])
->values()
->all();
}
return [
'fields' => [
[
'id' => 'enabled',
'inputType' => InputTypeEnum::TOGGLE,
'label' => '',
'value' => $report?->getStatus() ?? false,
],
[
'id' => 'report_name',
'inputType' => InputTypeEnum::TEXT,
'label' => 'Name',
'placeholder' => 'Enter name',
'required' => true,
'validation' => ['maxLength' => 50],
'value' => $report?->getCustomName() ?? '',
...
|
57994
|
NULL
|
NULL
|
NULL
|
|
58017
|
2043
|
1
|
2026-05-19T11:26:29.139749+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779189989139_m2.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskAnythingPromptServiceTest
Run 'AskAnythingPromptServiceTest'
Debug 'AskAnythingPromptServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
100
3
34
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Kiosk\AutomatedReports;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Carbon\Exceptions\InvalidFormatException;
use DateTime;
use DateTimeInterface;
use DateTimeZone;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\ActivitySearch\FilterDefinition\InputTypeEnum;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Partner;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Throwable;
class AutomatedReportsService
{
public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';
public const string TYPE_ASK_JIMINNY = 'ask_jiminny';
/**
* Standard report types (used by kiosk for existing automated reports).
*/
// @TODO this will add filter, however if we need to control feature by FF we need conditional logic
public const array TYPES = [
['id' => 'exec_summary', 'name' => 'Exec Summary'],
['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],
['id' => 'product_feedback', 'name' => 'Product Feedback'],
['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],
// ['id' => 'questions', 'name' => 'Questions'],
// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],
];
public const array ALL_TYPES = [
...self::TYPES,
['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],
];
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';
/**
* Frequencies for standard (non-Ask Jiminny) reports.
*/
public const array FREQUENCIES = [
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
/**
* Frequencies for Ask Jiminny reports.
*/
public const array ASK_JIMINNY_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
];
public const string MEDIA_TYPE_PDF = 'pdf';
public const string MEDIA_TYPE_PODCAST = 'podcast';
public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];
public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];
public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];
public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];
public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];
public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];
public const int SENT_REPORT_AT_HOURS = 5;
public const string PDF_KEY = 'pdf';
public const string AUDIO_KEY = 'audio';
private const array ALL_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
private const string S3_DIR = 'reports';
private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];
private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly GroupRepository $groupRepository,
private readonly UserRepository $userRepository,
private readonly StageRepository $stageRepository,
private readonly DealStagesService $dealStagesService,
private readonly RecipientsService $recipientsService,
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly Webhook $webhookService,
private readonly BusDispatcher $dispatcher,
private readonly ActivityTypeService $activityTypeService,
private readonly PlaybookCategoryRepository $playbookCategoryRepository,
private readonly AskAnythingPromptService $askAnythingPromptService,
private readonly SearchRepository $activitySearchRepository,
private readonly AskAnythingRepository $askAnythingRepository,
) {
}
public static function getTypes(): array
{
$types = self::TYPES;
return array_map(static function ($type) {
return $type['id'];
}, $types);
}
public static function getCallTypes(): array
{
return array_map(static function ($callType) {
return $callType['id'];
}, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);
}
public static function getFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::FREQUENCIES);
}
// front-facing structure
public function getReportEnabledFieldData(bool $value = false): array
{
return [
'id' => 'report_enabled',
'label' => '',
'inputType' => InputTypeEnum::TOGGLE,
'value' => $value,
];
}
// Organizations = Teams
public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array
{
$options = $this->getTeams(partner: $partner);
if ($shortVersion) {
return [
'id' => 'organization',
'label' => 'Organization',
'options' => $options,
];
}
return [
'id' => 'organization',
'label' => 'Organization',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $options,
'value' => $value,
'dependencies' => [
'teams',
'deal_stage_at_call',
'current_deal_stage',
'recipients',
ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,
],
'dependsOn' => [],
];
}
// Teams = Groups
public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array
{
if ($shortVersion) {
return [
'id' => 'teams',
'label' => 'Team',
'options' => $options,
];
}
return [
'id' => 'teams',
'label' => 'Team',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => false,
'placeholder' => 'Select',
'options' => $options,
'value' => $value, // value should be an array of objects {id, name}
'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],
'dependsOn' => [],
];
}
public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array
{
$types = [];
if ($team instanceof Team) {
if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
$types = self::TYPES;
}
if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {
$types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];
}
} else {
$types = self::TYPES;
}
if ($shortVersion) {
return [
'id' => 'report_type',
'label' => 'Report Type',
'options' => $types,
];
}
return [
'id' => 'report_type',
'label' => 'Report Type',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $types,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getFrequencyFieldData(?string $value = null): array
{
return [
'id' => 'frequency',
'label' => 'Frequency',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => self::FREQUENCIES,
'value' => $value,
'dependencies' => ['period'],
'dependsOn' => [],
];
}
public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array
{
return [
'id' => 'period',
'label' => 'Select one-off period',
'inputType' => InputTypeEnum::DATE_RANGE,
'required' => true,
'placeholder' => 'Select',
'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],
'queryParams' => [
'startDate' => 'start_date_period',
'endDate' => 'end_date_period',
],
'dependencies' => [],
'dependsOn' => ['frequency'],
];
}
public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array
{
return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);
}
public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);
}
public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);
}
public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'deal_value',
'label' => 'Deal Value',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_deal_value',
'max' => 'max_deal_value',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array
{
$value = [];
$conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;
$dialerOn && $value[] = self::CALL_TYPE_DIALER;
return [
'id' => 'call_type',
'label' => 'Call Type',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => [
self::CALL_TYPE_CONFERENCE,
self::CALL_TYPE_DIALER,
],
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getMediaTypeFieldData(?AutomatedReport $report = null): array
{
$value = [];
if ($report) {
$value = $this->transformMediaTypes($report);
}
return [
'id' => 'media_types',
'label' => 'Export as',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => self::MEDIA_TYPE_OBJECTS,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'call_duration',
'label' => 'Call Duration',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_call_duration',
'max' => 'max_call_duration',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getRecipientsFieldData(?Team $team = null, array $value = []): array
{
return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);
}
public function getJiminnyRecipientsFieldData(array $value = []): array
{
return $this->recipientsService->getJiminnyRecipientsFieldData($value);
}
public function getAdditionalPromptInputFieldData(?string $value = null): array
{
return [
'id' => 'additional_prompt_input',
'label' => 'Special requirements',
'inputType' => InputTypeEnum::TEXTAREA,
'required' => false,
'placeholder' => 'What should be the focus of the report?',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCustomReportNameFieldData(?string $value = null): array
{
return [
'id' => 'custom_name',
'label' => 'Custom report name',
'inputType' => InputTypeEnum::TEXT,
'required' => false,
'placeholder' => 'Enter custom name',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
// data providers
public function getTeams(?Partner $partner = null): array
{
$teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);
$teamData = [];
foreach ($teams as $team) {
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
continue;
}
$teamData[] = $this->transformTeam($team);
}
return $teamData;
}
public function getTeamGroups(string $teamUuid): array
{
$data = [];
$team = $this->getTeam($teamUuid);
if ($team !== null) {
$groups = $team->groups()->get();
foreach ($groups as $group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
];
}
}
return $data;
}
public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array
{
$data = [];
$teams = $this->getTeams(partner: $partner);
foreach ($teams as $team) {
if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {
continue;
}
$data[] = [
'label' => $team['name'],
'groups' => $this->getTeamGroups($team['id']),
];
}
return $data;
}
public function getTeam(string $teamUuid): ?Team
{
return $this->teamRepository->idOrUuid($teamUuid);
}
public function getTeamById(int $teamId): ?Team
{
return $this->teamRepository->find($teamId);
}
public function getGroupsUuids(AutomatedReport $report): array
{
$uuids = [];
$reportGroups = $report->getGroups();
foreach ($reportGroups as $groupId) {
if ($group = $this->groupRepository->find($groupId)) {
$uuids[] = $group->getUuid();
}
}
return $uuids;
}
public function getPlaybookCategoriesUuids(AutomatedReport $report): array
{
$uuids = [];
$playbookCategories = $report->getPlaybookCategories();
foreach ($playbookCategories as $id) {
if ($category = $this->playbookCategoryRepository->find($id)) {
$uuids[] = $category->getUuid();
}
}
return $uuids;
}
public function getDealAtCallStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getDealAtCallStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getCurrentDealStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getCurrentDealStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getRecipients());
}
public function getJiminnyUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function extractUserUuids(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => $user->getUuid())
->values()
->all();
}
// get mail data
public function getRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getRecipients());
}
/**
* @return array<UserContract>
*/
public function getRecipientUserObjects(AutomatedReport $report): array
{
$userIds = $report->getRecipients()['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->values()
->all();
}
private function getJiminnyRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function buildRecipientUsers(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => [
'email' => $user->getEmailAddress(),
'name' => $user->getName(),
'timezone' => $user->getTimezone()->getName(),
])
->values()
->all();
}
public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array
{
if ($report->isAskJiminnyReport()) {
$recipients = $this->resolveAskJiminnyRecipients($report);
} else {
$recipients = $this->getRecipientUsers($report);
if ($includeJiminny) {
$recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));
}
}
$emails = [];
return array_values(array_filter(
$recipients,
static function ($recipient) use (&$emails) {
if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {
return false;
}
$emails[] = $recipient['email'];
return true;
}
));
}
private function resolveAskJiminnyRecipients(AutomatedReport $report): array
{
$recipients = [];
$creator = $report->getCreator();
if ($creator !== null) {
$recipients[] = [
'email' => $creator->getEmailAddress(),
'name' => $creator->getName(),
'timezone' => $creator->getTimezone()->getName(),
];
}
return array_merge(
$recipients,
$this->buildRecipientUsers($report->getRecipients()),
$this->getGroupRecipientUsers($report),
);
}
private function getGroupRecipientUsers(AutomatedReport $report): array
{
$users = [];
foreach ($report->getGroups() as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group === null) {
continue;
}
foreach ($group->getMembers() as $member) {
$users[] = [
'email' => $member->getEmailAddress(),
'name' => $member->getName(),
'timezone' => $member->getTimezone()->getName(),
];
}
}
return $users;
}
public function getReportTypeName(AutomatedReportResult $report): string
{
$type = $report->getReport()->getType();
$getType = $this->transformReportType($type);
return $getType['name'];
}
public function getReportPeriodName(AutomatedReportResult $report): string
{
$from = $report->getFromDate();
$to = $report->getToDate();
$frequency = $report->getReport()->getFrequency();
if ($from === null || $to === null) {
if (! $report->getReport()->isAskJiminnyReport()) {
$invalidPeriod = $from === null ? 'from' : 'to';
throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);
}
$timezone = $report->getReport()->getCreator()?->getTimezone();
$period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);
$from = $period['fromDate'];
$to = $period['toDate'];
}
return $this->formatReportPeriodName($frequency, $from, $to);
}
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 getReportTeamsName(AutomatedReportResult $report): string
{
$groups = $report->getGroups();
if (empty($groups)) {
return 'All';
}
// Get group names from repository
$groupNames = [];
foreach ($groups as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group) {
$groupNames[] = $group->getName();
}
}
if (count($groupNames) === 1) {
// Single team format
$teamsName = $groupNames[0];
} else {
// Multiple teams format
$teamsName = implode(', ', $groupNames);
}
return $teamsName;
}
public function getReportFileName(AutomatedReportResult $report): string
{
$customName = $report->getReport()->getCustomName();
$periodName = $this->getReportPeriodName($report);
$filenameSuffix = $this->getFilenameSuffix($report);
if ($customName) {
if ($filenameSuffix) {
$customName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$customName} - {$periodName}");
}
$baseName = $this->getReportTypeName($report);
if ($filenameSuffix) {
$baseName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}");
}
public function getReportFileNameWithExtension(AutomatedReportResult $result): string
{
$extension = $this->getMediaTypeMetadata($result)['extension'];
return $this->getReportFileName($result) . '.' . $extension;
}
public function sanitizeFileName(string $fileName): string
{
return str_replace(['/', '\\'], '-', $fileName);
}
public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool
{
$recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);
if (in_array($user->getId(), $recipientIds, true)) {
return true;
}
if ($report->isAskJiminnyReport()) {
$groupId = $user->getGroupId();
if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {
return true;
}
}
return false;
}
public function transformReportResults(Collection $automatedReportResults): array
{
$data = [];
foreach ($automatedReportResults as $automatedReportResult) {
/** @var AutomatedReportResult $automatedReportResult */
$report = $automatedReportResult->getReport();
$createdBy = $report->getCreator();
$creator = [
'id' => $createdBy?->getUuid(),
'name' => $createdBy?->getName(),
'email' => $createdBy?->getEmailAddress(),
'photoUrl' => $createdBy?->getPhotoUrl(),
];
$data[] = [
'id' => $automatedReportResult->getUuid(),
'name' => $automatedReportResult->getName(),
'frequency' => $this->transformFrequency($report->getFrequency()),
'recipients' => $this->buildRecipients($report),
'report_type' => $this->transformReportType($report->getType()),
'media_type' => $automatedReportResult->getMediaType(),
'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),
'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),
'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),
'creator' => $creator,
];
}
return $data;
}
private function buildRecipients(AutomatedReport $report): array
{
$creatorUuid = $report->getCreator()?->getUuid();
$recipients = array_values(array_filter(
$this->transformRecipients($report->getRecipients()),
static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,
));
if (! $report->isAskJiminnyReport()) {
return $recipients;
}
return [
...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),
...$recipients,
];
}
public function hasCallTypeConference(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);
}
public function hasCallTypeDialer(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);
}
// transformers
private function transformTeam(Team $team): array
{
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
return [];
}
return [
'id' => $team->getUuid(),
'name' => $team->getName(),
];
}
private function transformReportFullView(AutomatedReport $report): array
{
$base = $this->transformReportBase($report);
return $report->getType() === self::TYPE_ASK_JIMINNY
? $base + $this->transformAskJiminnyFields($report)
: $base + $this->transformStandardReportFields($report);
}
private function transformReportBase(AutomatedReport $report): array
{
return [
'id' => $report->getUuid(),
'organization' => $this->transformOrganization(team: $report->getTeam()),
'report_type' => $this->transformReportType($report->getType()),
'frequency' => $this->transformFrequency($report->getFrequency()),
];
}
private function transformStandardReportFields(AutomatedReport $report): array
{
$team = $report->getTeam();
return [
'report_enabled' => $report->getStatus(),
'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),
'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),
'deal_value_min' => $report->getDealValueMin(),
'deal_value_max' => $report->getDealValueMax(),
'call_types' => $this->transformCallType($report->getCallTypes()),
'media_types' => $this->transformMediaTypes($report),
'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),
'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),
'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),
'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),
'recipients' => $this->transformRecipients($report->getRecipients()),
'created_by' => $this->transformCreator($report->getCreator()),
'additional_prompt_input' => $report->getAdditionalPromptInput(),
'custom_name' => $report->getCustomName(),
'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),
'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),
'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),
];
}
private function transformAskJiminnyFields(AutomatedReport $report): array
{
$team = $report->getTeam();
$creatorId = $report->getAttribute('created_by');
$explicitUserIds = array_values(array_filter(
$report->getRecipients()['users'] ?? [],
static fn ($id) => $id !== $creatorId
));
return [
'report_name' => $report->getCustomName(),
'enabled' => $report->getStatus(),
'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),
'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),
'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),
'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),
];
}
private function transformOrganization(?Team $team): array
{
return [
'id' => $team?->getUuid(),
'name' => $team?->getName(),
];
}
private function transformReportType(string $type): array
{
foreach (self::ALL_TYPES as $typeItem) {
if ($typeItem['id'] === $type) {
return $typeItem;
}
}
return [];
}
private function transformCallType(array $types): array
{
$result = [];
$callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];
foreach ($types as $type) {
foreach ($callTypes as $callTypeItem) {
if ($callTypeItem['id'] === $type) {
$result[] = $callTypeItem;
break;
}
}
}
return $result;
}
private function transformMediaTypes(AutomatedReport $report): array
{
$values = [];
foreach ($report->getMediaTypes() as $mediaType) {
if (! in_array($mediaType, self::MEDIA_TYPES, true)) {
continue;
}
$values[] = match ($mediaType) {
self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,
self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,
};
}
return $values;
}
private function transformFrequency(string $frequency): array
{
foreach (self::ALL_FREQUENCIES as $frequencyItem) {
if ($frequencyItem['id'] === $frequency) {
return $frequencyItem;
}
}
return [];
}
public function transformDurationToMinutes(?int $duration): ?int
{
if (! $duration) {
return null;
}
return (int) ($duration / 60);
}
private function transformGroups(?Team $team, array $groupsIds): array
{
if (empty($groupsIds) || ! $team) {
return [];
}
$data = [];
foreach ($groupsIds as $groupId) {
$group = $team->groups()->where('id', $groupId)->first();
if ($group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
'photoUrl' => $group->getPhotoUrl(),
];
}
}
return $data;
}
private function transformStages(?Team $team, array $stagesIds): array
{
if (empty($stagesIds) || ! $team) {
return [];
}
$data = [];
foreach ($stagesIds as $stageId) {
$stage = $team->stages()->where('id', $stageId)->first();
if ($stage) {
$data[] = [
'id' => $stage->getUuid(),
'name' => $stage->getName(),
];
}
}
return $data;
}
private function transformRecipients(array $recipients): array
{
$users = [];
foreach ($recipients['users'] ?? [] as $userId) {
$users[] = $this->transformUser($userId);
}
return $users;
}
private function transformCreator(?User $user): ?array
{
if ($user === null) {
return null;
}
return $this->transformUser($user->getId());
}
private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array
{
if ($prompt === null) {
return null;
}
return [
'id' => $prompt->getUuid(),
'name' => $prompt->getTitle(),
];
}
private function transformSafeSearch(?Search $search): ?array
{
if ($search === null) {
return null;
}
return [
'id' => $search->getUuid(),
'name' => $search->getName(),
];
}
private function transformUser(int $userId): array
{
/* @var ?User $user */
$user = $this->userRepository->find($userId);
return [
'id' => $user?->getUuid(),
'name' => $user?->getName(),
'email' => $user?->getEmailAddress(),
'photoUrl' => $user?->getPhotoUrl(),
];
}
public function create(array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$validatedData['created_by'] = auth()->id();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
public function update(string $uuid, array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$report = $this->automatedReportsRepository->findByUuid($uuid);
if (! $report) {
throw new InvalidArgumentException('Report not found');
}
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
/**
* Create an Ask Jiminny report.
*/
public function createAskJiminnyReport(array $data, User $creator): array
{
$validatedData = $this->validateAskJiminnyReportData($data, $creator);
$validatedData['created_by'] = $creator->getId();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
return $this->transformReportFullView($automatedReport);
}
/**
* Update an Ask Jiminny report.
*/
public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array
{
if (! $report->isAskJiminnyReport()) {
throw new InvalidArgumentException('Report is not an Ask Jiminny report');
}
$validatedData = $this->validateAskJiminnyReportData($data, $user);
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
return $this->transformReportFullView($automatedReport);
}
public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array
{
if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {
throw new InvalidArgumentException(
'This report is missing a saved search or prompt. ' .
'Edit the report to complete the setup before enabling it.'
);
}
$this->automatedReportsRepository->update($report, ['status' => $status]);
return $this->transformReportFullView($report->fresh());
}
/**
* Validate and transform data for Ask Jiminny reports.
*/
private function validateAskJiminnyReportData(array $data, User $user): array
{
// Validate name
$name = trim($data['report_name'] ?? '');
if (empty($name)) {
throw new InvalidArgumentException('Report name is required');
}
if (mb_strlen($name) > 50) {
throw new InvalidArgumentException('Report name must be 50 characters or less');
}
// Validate frequency (only daily, weekly, monthly for Ask Jiminny)
$frequency = $data['frequency'] ?? null;
$askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];
if (! in_array($frequency, $askJiminnyFrequencies, true)) {
throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');
}
// Validate expiration date
$expiresAt = $data['expires_on'] ?? null;
if (empty($expiresAt)) {
throw new InvalidArgumentException('Expiration date is required');
}
try {
$expiresAtDate = Carbon::parse($expiresAt);
} catch (InvalidFormatException $e) {
throw new InvalidArgumentException('Expiration date format is invalid');
}
$maxExpiration = Carbon::now()->addYear()->endOfDay();
if ($expiresAtDate->gt($maxExpiration)) {
throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');
}
if ($expiresAtDate->isPast()) {
throw new InvalidArgumentException('Expiration date cannot be in the past');
}
// Validate saved search
$activitySearchId = $data['saved_search'] ?? null;
if (empty($activitySearchId)) {
throw new InvalidArgumentException('Saved search is required');
}
$savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);
if (! $savedSearch) {
throw new InvalidArgumentException('Saved search not found or does not belong to you');
}
// Validate saved prompt
$askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;
if (empty($askAnythingPromptId)) {
throw new InvalidArgumentException('Ask Jiminny prompt is required');
}
$prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);
if (! $prompt) {
throw new InvalidArgumentException('Ask Jiminny prompt not found');
}
// Validate status
$status = $data['enabled'] ?? false;
$recipientUserIds = [$user->getId()];
if (! empty($data['share_users'])) {
$sharedUserIds = $this->validateAndGetUserIdsByTeam(
$user->team,
(array) $data['share_users']
);
$recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);
}
$sharedGroupIds = [];
if (! empty($data['share_teams'])) {
$sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);
}
$recipientUserIds = array_values(array_unique($recipientUserIds));
return [
'team_id' => $user->getTeamId(),
'type' => self::TYPE_ASK_JIMINNY,
'status' => (bool) $status,
'frequency' => $frequency,
'custom_name' => $name,
'activity_search_id' => $savedSearch->getId(),
'ask_anything_prompt_id' => $prompt->getId(),
'expires_at' => $expiresAtDate->toDateString(),
'media_types' => [self::MEDIA_TYPE_PDF],
'call_types' => [],
'recipients' => ['users' => $recipientUserIds],
'groups' => $sharedGroupIds,
];
}
public static function getAskJiminnyFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::ASK_JIMINNY_FREQUENCIES);
}
public function getAskJiminnyReportFilters(User $user): array
{
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
return [
[
'id' => 'prompt',
'label' => 'Prompt',
'options' => $prompts,
],
[
'id' => 'saved_search',
'label' => 'Saved Search',
'options' => $savedSearches,
],
];
}
public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array
{
$team = $user->getTeam();
$userTimezone = $user->getTimezone();
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
$teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [
'id' => $group->getUuid(),
'name' => $group->getName(),
])->values()->all();
$shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];
$sharedTeamsValue = [];
$sharedUsersValue = [];
if ($report) {
$sharedTeamsValue = $this->transformGroups($team, $report->getGroups());
$recipientUserIds = $report->getRecipients()['users'] ?? [];
$creatorId = $report->getAttribute('created_by');
$sharedUserIds = array_values(array_filter(
$recipientUserIds,
static fn ($id) => $id !== $creatorId
));
$sharedUsersValue = collect($sharedUserIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (User $u) => [
'id' => $u->getUuid(),
'name' => $u->getName(),
])
->values()
->all();
}
return [
'fields' => [
[
'id' => 'enabled',
'inputType' => InputTypeEnum::TOGGLE,
'label' => '',
'value' => $report?->getStatus() ?? false,
],
[
'id' => 'report_name',
'inputType' => InputTypeEnum::TEXT,
'label' => 'Name',
'placeholder' => 'Enter name',
'required' => true,
'validation' => ['maxLength' => 50],
'value' => $report?->getCustomName() ?? '',
...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.10405585,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects<br/>Some incoming commits are not fetched<br/>","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.8194814,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskAnythingPromptServiceTest","depth":6,"bounds":{"left":0.83477396,"top":0.019952115,"width":0.080784574,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskAnythingPromptServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskAnythingPromptServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"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},"on_screen":true,"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},"on_screen":true,"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},"on_screen":true,"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},"on_screen":true,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"100","depth":4,"bounds":{"left":0.37599733,"top":0.15003991,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.3899601,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"34","depth":4,"bounds":{"left":0.39993352,"top":0.15003991,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.4192154,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"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\\Services\\Kiosk\\AutomatedReports;\n\nuse Carbon\\CarbonImmutable;\nuse Carbon\\CarbonInterface;\nuse Carbon\\Exceptions\\InvalidFormatException;\nuse DateTime;\nuse DateTimeInterface;\nuse DateTimeZone;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\InputTypeEnum;\nuse Jiminny\\Component\\AskAnything\\AskAnythingPromptService;\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Contracts\\Repositories\\PlaybookCategoryRepository;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\ModelNotFoundException;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Partner;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Repositories\\GroupRepository;\nuse Jiminny\\Repositories\\SearchRepository;\nuse Jiminny\\Repositories\\StageRepository;\nuse Throwable;\n\nclass AutomatedReportsService\n{\n public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';\n public const string TYPE_ASK_JIMINNY = 'ask_jiminny';\n\n /**\n * Standard report types (used by kiosk for existing automated reports).\n */\n // @TODO this will add filter, however if we need to control feature by FF we need conditional logic\n public const array TYPES = [\n ['id' => 'exec_summary', 'name' => 'Exec Summary'],\n ['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],\n ['id' => 'product_feedback', 'name' => 'Product Feedback'],\n ['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],\n// ['id' => 'questions', 'name' => 'Questions'],\n// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],\n ];\n\n public const array ALL_TYPES = [\n ...self::TYPES,\n ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],\n ];\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\n /**\n * Frequencies for standard (non-Ask Jiminny) reports.\n */\n public const array FREQUENCIES = [\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n\n /**\n * Frequencies for Ask Jiminny reports.\n */\n public const array ASK_JIMINNY_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ];\n\n public const string MEDIA_TYPE_PDF = 'pdf';\n public const string MEDIA_TYPE_PODCAST = 'podcast';\n public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];\n public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];\n public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];\n public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];\n\n public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];\n public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];\n public const int SENT_REPORT_AT_HOURS = 5;\n public const string PDF_KEY = 'pdf';\n public const string AUDIO_KEY = 'audio';\n\n private const array ALL_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n private const string S3_DIR = 'reports';\n private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];\n private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];\n\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly GroupRepository $groupRepository,\n private readonly UserRepository $userRepository,\n private readonly StageRepository $stageRepository,\n private readonly DealStagesService $dealStagesService,\n private readonly RecipientsService $recipientsService,\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly Webhook $webhookService,\n private readonly BusDispatcher $dispatcher,\n private readonly ActivityTypeService $activityTypeService,\n private readonly PlaybookCategoryRepository $playbookCategoryRepository,\n private readonly AskAnythingPromptService $askAnythingPromptService,\n private readonly SearchRepository $activitySearchRepository,\n private readonly AskAnythingRepository $askAnythingRepository,\n ) {\n }\n\n public static function getTypes(): array\n {\n $types = self::TYPES;\n\n return array_map(static function ($type) {\n return $type['id'];\n }, $types);\n }\n\n public static function getCallTypes(): array\n {\n return array_map(static function ($callType) {\n return $callType['id'];\n }, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);\n }\n\n public static function getFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::FREQUENCIES);\n }\n\n // front-facing structure\n public function getReportEnabledFieldData(bool $value = false): array\n {\n return [\n 'id' => 'report_enabled',\n 'label' => '',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'value' => $value,\n ];\n }\n\n // Organizations = Teams\n public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array\n {\n $options = $this->getTeams(partner: $partner);\n\n if ($shortVersion) {\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value,\n 'dependencies' => [\n 'teams',\n 'deal_stage_at_call',\n 'current_deal_stage',\n 'recipients',\n ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,\n ],\n 'dependsOn' => [],\n ];\n }\n\n // Teams = Groups\n public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array\n {\n if ($shortVersion) {\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value, // value should be an array of objects {id, name}\n 'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],\n 'dependsOn' => [],\n ];\n }\n\n public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array\n {\n $types = [];\n if ($team instanceof Team) {\n if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n $types = self::TYPES;\n }\n if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {\n $types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];\n }\n } else {\n $types = self::TYPES;\n }\n\n if ($shortVersion) {\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'options' => $types,\n ];\n }\n\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $types,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getFrequencyFieldData(?string $value = null): array\n {\n return [\n 'id' => 'frequency',\n 'label' => 'Frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::FREQUENCIES,\n 'value' => $value,\n 'dependencies' => ['period'],\n 'dependsOn' => [],\n ];\n }\n\n public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array\n {\n return [\n 'id' => 'period',\n 'label' => 'Select one-off period',\n 'inputType' => InputTypeEnum::DATE_RANGE,\n 'required' => true,\n 'placeholder' => 'Select',\n 'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],\n 'queryParams' => [\n 'startDate' => 'start_date_period',\n 'endDate' => 'end_date_period',\n ],\n 'dependencies' => [],\n 'dependsOn' => ['frequency'],\n ];\n }\n\n public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array\n {\n return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);\n }\n\n public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);\n }\n\n public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);\n }\n\n public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'deal_value',\n 'label' => 'Deal Value',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_deal_value',\n 'max' => 'max_deal_value',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array\n {\n $value = [];\n $conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;\n $dialerOn && $value[] = self::CALL_TYPE_DIALER;\n\n return [\n 'id' => 'call_type',\n 'label' => 'Call Type',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => [\n self::CALL_TYPE_CONFERENCE,\n self::CALL_TYPE_DIALER,\n ],\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getMediaTypeFieldData(?AutomatedReport $report = null): array\n {\n $value = [];\n\n if ($report) {\n $value = $this->transformMediaTypes($report);\n }\n\n return [\n 'id' => 'media_types',\n 'label' => 'Export as',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => self::MEDIA_TYPE_OBJECTS,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'call_duration',\n 'label' => 'Call Duration',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_call_duration',\n 'max' => 'max_call_duration',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getRecipientsFieldData(?Team $team = null, array $value = []): array\n {\n return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);\n }\n\n public function getJiminnyRecipientsFieldData(array $value = []): array\n {\n return $this->recipientsService->getJiminnyRecipientsFieldData($value);\n }\n\n public function getAdditionalPromptInputFieldData(?string $value = null): array\n {\n return [\n 'id' => 'additional_prompt_input',\n 'label' => 'Special requirements',\n 'inputType' => InputTypeEnum::TEXTAREA,\n 'required' => false,\n 'placeholder' => 'What should be the focus of the report?',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCustomReportNameFieldData(?string $value = null): array\n {\n return [\n 'id' => 'custom_name',\n 'label' => 'Custom report name',\n 'inputType' => InputTypeEnum::TEXT,\n 'required' => false,\n 'placeholder' => 'Enter custom name',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n // data providers\n public function getTeams(?Partner $partner = null): array\n {\n $teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);\n\n $teamData = [];\n foreach ($teams as $team) {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n continue;\n }\n\n $teamData[] = $this->transformTeam($team);\n }\n\n return $teamData;\n }\n\n public function getTeamGroups(string $teamUuid): array\n {\n $data = [];\n $team = $this->getTeam($teamUuid);\n\n if ($team !== null) {\n $groups = $team->groups()->get();\n\n foreach ($groups as $group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array\n {\n $data = [];\n $teams = $this->getTeams(partner: $partner);\n\n foreach ($teams as $team) {\n if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {\n continue;\n }\n\n $data[] = [\n 'label' => $team['name'],\n 'groups' => $this->getTeamGroups($team['id']),\n ];\n }\n\n return $data;\n }\n\n public function getTeam(string $teamUuid): ?Team\n {\n return $this->teamRepository->idOrUuid($teamUuid);\n }\n\n public function getTeamById(int $teamId): ?Team\n {\n return $this->teamRepository->find($teamId);\n }\n\n public function getGroupsUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportGroups = $report->getGroups();\n foreach ($reportGroups as $groupId) {\n if ($group = $this->groupRepository->find($groupId)) {\n $uuids[] = $group->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getPlaybookCategoriesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $playbookCategories = $report->getPlaybookCategories();\n foreach ($playbookCategories as $id) {\n if ($category = $this->playbookCategoryRepository->find($id)) {\n $uuids[] = $category->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getDealAtCallStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getDealAtCallStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getCurrentDealStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getCurrentDealStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getRecipients());\n }\n\n public function getJiminnyUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function extractUserUuids(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => $user->getUuid())\n ->values()\n ->all();\n }\n\n // get mail data\n public function getRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getRecipients());\n }\n\n /**\n * @return array<UserContract>\n */\n public function getRecipientUserObjects(AutomatedReport $report): array\n {\n $userIds = $report->getRecipients()['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->values()\n ->all();\n }\n\n private function getJiminnyRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function buildRecipientUsers(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => [\n 'email' => $user->getEmailAddress(),\n 'name' => $user->getName(),\n 'timezone' => $user->getTimezone()->getName(),\n ])\n ->values()\n ->all();\n }\n\n public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array\n {\n if ($report->isAskJiminnyReport()) {\n $recipients = $this->resolveAskJiminnyRecipients($report);\n } else {\n $recipients = $this->getRecipientUsers($report);\n if ($includeJiminny) {\n $recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));\n }\n }\n\n $emails = [];\n\n return array_values(array_filter(\n $recipients,\n static function ($recipient) use (&$emails) {\n if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {\n return false;\n }\n $emails[] = $recipient['email'];\n\n return true;\n }\n ));\n }\n\n private function resolveAskJiminnyRecipients(AutomatedReport $report): array\n {\n $recipients = [];\n\n $creator = $report->getCreator();\n if ($creator !== null) {\n $recipients[] = [\n 'email' => $creator->getEmailAddress(),\n 'name' => $creator->getName(),\n 'timezone' => $creator->getTimezone()->getName(),\n ];\n }\n\n return array_merge(\n $recipients,\n $this->buildRecipientUsers($report->getRecipients()),\n $this->getGroupRecipientUsers($report),\n );\n }\n\n private function getGroupRecipientUsers(AutomatedReport $report): array\n {\n $users = [];\n foreach ($report->getGroups() as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group === null) {\n continue;\n }\n foreach ($group->getMembers() as $member) {\n $users[] = [\n 'email' => $member->getEmailAddress(),\n 'name' => $member->getName(),\n 'timezone' => $member->getTimezone()->getName(),\n ];\n }\n }\n\n return $users;\n }\n\n public function getReportTypeName(AutomatedReportResult $report): string\n {\n $type = $report->getReport()->getType();\n\n $getType = $this->transformReportType($type);\n\n return $getType['name'];\n }\n\n public function getReportPeriodName(AutomatedReportResult $report): string\n {\n $from = $report->getFromDate();\n $to = $report->getToDate();\n $frequency = $report->getReport()->getFrequency();\n\n if ($from === null || $to === null) {\n if (! $report->getReport()->isAskJiminnyReport()) {\n $invalidPeriod = $from === null ? 'from' : 'to';\n\n throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);\n }\n\n $timezone = $report->getReport()->getCreator()?->getTimezone();\n $period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);\n $from = $period['fromDate'];\n $to = $period['toDate'];\n }\n\n return $this->formatReportPeriodName($frequency, $from, $to);\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 getReportTeamsName(AutomatedReportResult $report): string\n {\n $groups = $report->getGroups();\n\n if (empty($groups)) {\n return 'All';\n }\n\n // Get group names from repository\n $groupNames = [];\n foreach ($groups as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group) {\n $groupNames[] = $group->getName();\n }\n }\n\n if (count($groupNames) === 1) {\n // Single team format\n $teamsName = $groupNames[0];\n } else {\n // Multiple teams format\n $teamsName = implode(', ', $groupNames);\n }\n\n return $teamsName;\n }\n\n public function getReportFileName(AutomatedReportResult $report): string\n {\n $customName = $report->getReport()->getCustomName();\n $periodName = $this->getReportPeriodName($report);\n $filenameSuffix = $this->getFilenameSuffix($report);\n\n if ($customName) {\n if ($filenameSuffix) {\n $customName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$customName} - {$periodName}\");\n }\n\n $baseName = $this->getReportTypeName($report);\n\n if ($filenameSuffix) {\n $baseName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}\");\n }\n\n public function getReportFileNameWithExtension(AutomatedReportResult $result): string\n {\n $extension = $this->getMediaTypeMetadata($result)['extension'];\n\n return $this->getReportFileName($result) . '.' . $extension;\n }\n\n public function sanitizeFileName(string $fileName): string\n {\n return str_replace(['/', '\\\\'], '-', $fileName);\n }\n\n public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool\n {\n $recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);\n if (in_array($user->getId(), $recipientIds, true)) {\n return true;\n }\n\n if ($report->isAskJiminnyReport()) {\n $groupId = $user->getGroupId();\n if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {\n return true;\n }\n }\n\n return false;\n }\n\n public function transformReportResults(Collection $automatedReportResults): array\n {\n $data = [];\n foreach ($automatedReportResults as $automatedReportResult) {\n /** @var AutomatedReportResult $automatedReportResult */\n\n $report = $automatedReportResult->getReport();\n\n $createdBy = $report->getCreator();\n $creator = [\n 'id' => $createdBy?->getUuid(),\n 'name' => $createdBy?->getName(),\n 'email' => $createdBy?->getEmailAddress(),\n 'photoUrl' => $createdBy?->getPhotoUrl(),\n ];\n\n $data[] = [\n 'id' => $automatedReportResult->getUuid(),\n 'name' => $automatedReportResult->getName(),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n 'recipients' => $this->buildRecipients($report),\n 'report_type' => $this->transformReportType($report->getType()),\n 'media_type' => $automatedReportResult->getMediaType(),\n 'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),\n 'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),\n 'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),\n 'creator' => $creator,\n ];\n }\n\n return $data;\n }\n\n private function buildRecipients(AutomatedReport $report): array\n {\n $creatorUuid = $report->getCreator()?->getUuid();\n\n $recipients = array_values(array_filter(\n $this->transformRecipients($report->getRecipients()),\n static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,\n ));\n\n if (! $report->isAskJiminnyReport()) {\n return $recipients;\n }\n\n return [\n ...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),\n ...$recipients,\n ];\n }\n\n public function hasCallTypeConference(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);\n }\n\n public function hasCallTypeDialer(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);\n }\n\n // transformers\n private function transformTeam(Team $team): array\n {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n return [];\n }\n\n return [\n 'id' => $team->getUuid(),\n 'name' => $team->getName(),\n ];\n }\n\n private function transformReportFullView(AutomatedReport $report): array\n {\n $base = $this->transformReportBase($report);\n\n return $report->getType() === self::TYPE_ASK_JIMINNY\n ? $base + $this->transformAskJiminnyFields($report)\n : $base + $this->transformStandardReportFields($report);\n }\n\n private function transformReportBase(AutomatedReport $report): array\n {\n return [\n 'id' => $report->getUuid(),\n 'organization' => $this->transformOrganization(team: $report->getTeam()),\n 'report_type' => $this->transformReportType($report->getType()),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n ];\n }\n\n private function transformStandardReportFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n\n return [\n 'report_enabled' => $report->getStatus(),\n 'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),\n 'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),\n 'deal_value_min' => $report->getDealValueMin(),\n 'deal_value_max' => $report->getDealValueMax(),\n 'call_types' => $this->transformCallType($report->getCallTypes()),\n 'media_types' => $this->transformMediaTypes($report),\n 'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),\n 'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),\n 'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),\n 'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),\n 'recipients' => $this->transformRecipients($report->getRecipients()),\n 'created_by' => $this->transformCreator($report->getCreator()),\n 'additional_prompt_input' => $report->getAdditionalPromptInput(),\n 'custom_name' => $report->getCustomName(),\n 'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),\n 'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),\n 'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),\n ];\n }\n\n private function transformAskJiminnyFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n $creatorId = $report->getAttribute('created_by');\n $explicitUserIds = array_values(array_filter(\n $report->getRecipients()['users'] ?? [],\n static fn ($id) => $id !== $creatorId\n ));\n\n return [\n 'report_name' => $report->getCustomName(),\n 'enabled' => $report->getStatus(),\n 'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),\n 'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),\n 'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),\n 'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),\n ];\n }\n\n private function transformOrganization(?Team $team): array\n {\n return [\n 'id' => $team?->getUuid(),\n 'name' => $team?->getName(),\n ];\n }\n\n private function transformReportType(string $type): array\n {\n foreach (self::ALL_TYPES as $typeItem) {\n if ($typeItem['id'] === $type) {\n return $typeItem;\n }\n }\n\n return [];\n }\n\n private function transformCallType(array $types): array\n {\n $result = [];\n $callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];\n\n foreach ($types as $type) {\n foreach ($callTypes as $callTypeItem) {\n if ($callTypeItem['id'] === $type) {\n $result[] = $callTypeItem;\n\n break;\n }\n }\n }\n\n return $result;\n }\n\n private function transformMediaTypes(AutomatedReport $report): array\n {\n $values = [];\n\n foreach ($report->getMediaTypes() as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n continue;\n }\n\n $values[] = match ($mediaType) {\n self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,\n self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,\n };\n }\n\n return $values;\n }\n\n private function transformFrequency(string $frequency): array\n {\n foreach (self::ALL_FREQUENCIES as $frequencyItem) {\n if ($frequencyItem['id'] === $frequency) {\n return $frequencyItem;\n }\n }\n\n return [];\n }\n\n public function transformDurationToMinutes(?int $duration): ?int\n {\n if (! $duration) {\n return null;\n }\n\n return (int) ($duration / 60);\n }\n\n private function transformGroups(?Team $team, array $groupsIds): array\n {\n if (empty($groupsIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($groupsIds as $groupId) {\n $group = $team->groups()->where('id', $groupId)->first();\n\n if ($group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n 'photoUrl' => $group->getPhotoUrl(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformStages(?Team $team, array $stagesIds): array\n {\n if (empty($stagesIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($stagesIds as $stageId) {\n $stage = $team->stages()->where('id', $stageId)->first();\n\n if ($stage) {\n $data[] = [\n 'id' => $stage->getUuid(),\n 'name' => $stage->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformRecipients(array $recipients): array\n {\n $users = [];\n foreach ($recipients['users'] ?? [] as $userId) {\n $users[] = $this->transformUser($userId);\n }\n\n return $users;\n }\n\n private function transformCreator(?User $user): ?array\n {\n if ($user === null) {\n return null;\n }\n\n return $this->transformUser($user->getId());\n }\n\n private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array\n {\n if ($prompt === null) {\n return null;\n }\n\n return [\n 'id' => $prompt->getUuid(),\n 'name' => $prompt->getTitle(),\n ];\n }\n\n private function transformSafeSearch(?Search $search): ?array\n {\n if ($search === null) {\n return null;\n }\n\n return [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ];\n }\n\n private function transformUser(int $userId): array\n {\n /* @var ?User $user */\n $user = $this->userRepository->find($userId);\n\n return [\n 'id' => $user?->getUuid(),\n 'name' => $user?->getName(),\n 'email' => $user?->getEmailAddress(),\n 'photoUrl' => $user?->getPhotoUrl(),\n ];\n }\n\n public function create(array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $validatedData['created_by'] = auth()->id();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function update(string $uuid, array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $report = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $report) {\n throw new InvalidArgumentException('Report not found');\n }\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Create an Ask Jiminny report.\n */\n public function createAskJiminnyReport(array $data, User $creator): array\n {\n $validatedData = $this->validateAskJiminnyReportData($data, $creator);\n $validatedData['created_by'] = $creator->getId();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Update an Ask Jiminny report.\n */\n public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array\n {\n if (! $report->isAskJiminnyReport()) {\n throw new InvalidArgumentException('Report is not an Ask Jiminny report');\n }\n\n $validatedData = $this->validateAskJiminnyReportData($data, $user);\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array\n {\n if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {\n throw new InvalidArgumentException(\n 'This report is missing a saved search or prompt. ' .\n 'Edit the report to complete the setup before enabling it.'\n );\n }\n\n $this->automatedReportsRepository->update($report, ['status' => $status]);\n\n return $this->transformReportFullView($report->fresh());\n }\n\n /**\n * Validate and transform data for Ask Jiminny reports.\n */\n private function validateAskJiminnyReportData(array $data, User $user): array\n {\n // Validate name\n $name = trim($data['report_name'] ?? '');\n if (empty($name)) {\n throw new InvalidArgumentException('Report name is required');\n }\n if (mb_strlen($name) > 50) {\n throw new InvalidArgumentException('Report name must be 50 characters or less');\n }\n\n // Validate frequency (only daily, weekly, monthly for Ask Jiminny)\n $frequency = $data['frequency'] ?? null;\n $askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];\n if (! in_array($frequency, $askJiminnyFrequencies, true)) {\n throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');\n }\n\n // Validate expiration date\n $expiresAt = $data['expires_on'] ?? null;\n if (empty($expiresAt)) {\n throw new InvalidArgumentException('Expiration date is required');\n }\n\n try {\n $expiresAtDate = Carbon::parse($expiresAt);\n } catch (InvalidFormatException $e) {\n throw new InvalidArgumentException('Expiration date format is invalid');\n }\n $maxExpiration = Carbon::now()->addYear()->endOfDay();\n if ($expiresAtDate->gt($maxExpiration)) {\n throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');\n }\n if ($expiresAtDate->isPast()) {\n throw new InvalidArgumentException('Expiration date cannot be in the past');\n }\n\n // Validate saved search\n $activitySearchId = $data['saved_search'] ?? null;\n if (empty($activitySearchId)) {\n throw new InvalidArgumentException('Saved search is required');\n }\n $savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);\n if (! $savedSearch) {\n throw new InvalidArgumentException('Saved search not found or does not belong to you');\n }\n\n // Validate saved prompt\n $askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;\n if (empty($askAnythingPromptId)) {\n throw new InvalidArgumentException('Ask Jiminny prompt is required');\n }\n $prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);\n if (! $prompt) {\n throw new InvalidArgumentException('Ask Jiminny prompt not found');\n }\n\n // Validate status\n $status = $data['enabled'] ?? false;\n\n $recipientUserIds = [$user->getId()];\n\n if (! empty($data['share_users'])) {\n $sharedUserIds = $this->validateAndGetUserIdsByTeam(\n $user->team,\n (array) $data['share_users']\n );\n $recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);\n }\n\n $sharedGroupIds = [];\n if (! empty($data['share_teams'])) {\n $sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);\n }\n\n $recipientUserIds = array_values(array_unique($recipientUserIds));\n\n return [\n 'team_id' => $user->getTeamId(),\n 'type' => self::TYPE_ASK_JIMINNY,\n 'status' => (bool) $status,\n 'frequency' => $frequency,\n 'custom_name' => $name,\n 'activity_search_id' => $savedSearch->getId(),\n 'ask_anything_prompt_id' => $prompt->getId(),\n 'expires_at' => $expiresAtDate->toDateString(),\n 'media_types' => [self::MEDIA_TYPE_PDF],\n 'call_types' => [],\n 'recipients' => ['users' => $recipientUserIds],\n 'groups' => $sharedGroupIds,\n ];\n }\n\n public static function getAskJiminnyFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::ASK_JIMINNY_FREQUENCIES);\n }\n\n public function getAskJiminnyReportFilters(User $user): array\n {\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n return [\n [\n 'id' => 'prompt',\n 'label' => 'Prompt',\n 'options' => $prompts,\n ],\n [\n 'id' => 'saved_search',\n 'label' => 'Saved Search',\n 'options' => $savedSearches,\n ],\n ];\n }\n\n public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array\n {\n $team = $user->getTeam();\n $userTimezone = $user->getTimezone();\n\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n $teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ])->values()->all();\n\n $shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];\n\n $sharedTeamsValue = [];\n $sharedUsersValue = [];\n if ($report) {\n $sharedTeamsValue = $this->transformGroups($team, $report->getGroups());\n\n $recipientUserIds = $report->getRecipients()['users'] ?? [];\n $creatorId = $report->getAttribute('created_by');\n $sharedUserIds = array_values(array_filter(\n $recipientUserIds,\n static fn ($id) => $id !== $creatorId\n ));\n $sharedUsersValue = collect($sharedUserIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (User $u) => [\n 'id' => $u->getUuid(),\n 'name' => $u->getName(),\n ])\n ->values()\n ->all();\n }\n\n return [\n 'fields' => [\n [\n 'id' => 'enabled',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'label' => '',\n 'value' => $report?->getStatus() ?? false,\n ],\n [\n 'id' => 'report_name',\n 'inputType' => InputTypeEnum::TEXT,\n 'label' => 'Name',\n 'placeholder' => 'Enter name',\n 'required' => true,\n 'validation' => ['maxLength' => 50],\n 'value' => $report?->getCustomName() ?? '',\n ],\n [\n 'id' => 'frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Frequency',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::ASK_JIMINNY_FREQUENCIES,\n 'value' => $report ? $this->transformFrequency($report->getFrequency()) : null,\n ],\n [\n 'id' => 'expires_on',\n 'inputType' => InputTypeEnum::DATE,\n 'label' => 'Expires on',\n 'required' => true,\n 'placeholder' => 'Select',\n 'validation' => [\n 'minDate' => now($userTimezone)->toDateString(),\n 'maxDate' => now($userTimezone)->addYear()->toDateString(),\n ],\n 'value' => $report?->getExpiresAt()?->toDateString(),\n ],\n [\n 'id' => 'share_teams',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team',\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $teamGroups,\n 'value' => $sharedTeamsValue,\n ],\n [\n 'id' => 'share_users',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team member',\n 'required' => false,\n 'placeholder' => 'Select',\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'options' => $shareUsers,\n 'value' => $sharedUsersValue,\n ],\n [\n 'id' => 'saved_search',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Saved search',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $savedSearches,\n 'value' => $report && $report->getSavedSearch() ? [\n 'id' => $report->getSavedSearch()->getUuid(),\n 'name' => $report->getSavedSearch()->getName(),\n ] : null,\n ],\n [\n 'id' => 'ask_jiminny_prompt',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Ask Jiminny prompt',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $prompts,\n 'value' => $report && $report->getAskAnythingPrompt() ? [\n 'id' => $report->getAskAnythingPrompt()->getUuid(),\n 'name' => $report->getAskAnythingPrompt()->getTitle(),\n ] : null,\n ],\n ],\n ];\n }\n\n private function updateResultNames(AutomatedReport $automatedReport): void\n {\n $results = $this->automatedReportsRepository->getResultsByReport($automatedReport);\n\n foreach ($results as $result) {\n $result->update(['name' => $this->getReportFileName($result)]);\n }\n }\n\n public function updateStatus(string $uuid, array $data): array\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $automatedReport->update([\n 'status' => $status,\n ]);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n private function generateOneOffReport(AutomatedReport $automatedReport): void\n {\n // the scheduler handles all the other frequency types\n if ($automatedReport->getStatus() === false || $automatedReport->getFrequency() !== self::FREQUENCY_ONE_OFF) {\n return;\n }\n\n $this->dispatcher->dispatch(new RequestGenerateReportJob($automatedReport->getUuid()));\n }\n\n public function getReport(string $uuid, ?Partner $partner = null): AutomatedReport\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n if ($partner !== null && ! $partner->isDefaultPartner() && $automatedReport->team->partner_id !== $partner->getId()) {\n throw new ModelNotFoundException('Report not found');\n }\n\n return $automatedReport;\n }\n\n public function get(string $uuid, ?Partner $partner = null): array\n {\n $automatedReport = $this->getReport($uuid, $partner);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function list(string $sortColumn = 'created_at', string $sortDirection = 'desc', ?Partner $partner = null): array\n {\n $results = [];\n $collection = $this->automatedReportsRepository->getAllStandardReports($sortColumn, $sortDirection, $partner);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function listAskJiminnyReports(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): array {\n $results = [];\n $collection = $this->automatedReportsRepository->getAskJiminnyReportsByUser($user, $sortColumn, $sortDirection);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function delete(string $uuid): void\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $automatedReport->delete();\n }\n\n public function createReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n return $this->automatedReportsRepository->createResult(\n array_merge(\n [\n 'report_id' => $automatedReport->getId(),\n 'status' => AutomatedReportResult::STATUS_DEFAULT,\n ],\n $data\n )\n );\n }\n\n public function getOrCreateReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n $existing = $this->automatedReportsRepository->findLatestSameDayDefaultOrFailedResult($automatedReport);\n\n if ($existing !== null) {\n $existing->update(['status' => AutomatedReportResult::STATUS_DEFAULT]);\n\n return $existing;\n }\n\n return $this->createReportResult($automatedReport, $data);\n }\n\n public function getReportResult(string $resultUuid): AutomatedReportResult\n {\n $report = $this->automatedReportsRepository->findResultByUuid($resultUuid);\n\n if (! $report) {\n throw new ModelNotFoundException('Report Result not found');\n }\n\n return $report;\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return $this->automatedReportsRepository->findChildResult($result, $type);\n }\n\n // prophet API calls\n /**\n * @throws ApplicationException\n */\n public function getGenerateReportPayload(AutomatedReport $automatedReport, string $reportResultUuid): array\n {\n $period = $this->calculateFromAndToDate($automatedReport);\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n return [\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResultUuid,\n 'report_type' => $automatedReport->getType(),\n 'media_types' => $automatedReport->getMediaTypes(),\n 'from_date' => $fromDate->startOfDay()->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->endOfDay()->format(DateTimeInterface::RFC3339),\n 'group_ids' => $automatedReport->getGroups(),\n 'call_deal_stage' => $automatedReport->getDealAtCallStages(),\n 'current_deal_stage' => $automatedReport->getCurrentDealStages(),\n 'deal_min_value' => $automatedReport->getDealValueMin(),\n 'deal_max_value' => $automatedReport->getDealValueMax(),\n 'call_types' => $automatedReport->getCallTypes(),\n 'call_duration_min_seconds' => $automatedReport->getCallDurationMin(),\n 'call_duration_max_seconds' => $automatedReport->getCallDurationMax(),\n 'special_requirements' => $automatedReport->getAdditionalPromptInput(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->formatReportPeriodName(\n $automatedReport->getFrequency(),\n $fromDate,\n $toDate,\n ),\n 'playbook_categories' => $automatedReport->getPlaybookCategories(),\n 'custom_name' => $automatedReport->getCustomName(),\n ];\n }\n\n // $inputPayload - FE payload structure\n public function getActivitiesCountPayload(array $inputPayload): array\n {\n // Use validateAndTransformData to validate and normalize input\n $validatedData = $this->validateAndTransformData($inputPayload);\n $period = $this->calculateFromAndToDatePeriod(\n $validatedData['frequency'],\n Carbon::parse($validatedData['from']),\n Carbon::parse($validatedData['to']),\n );\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n // Create payload similar to getGenerateReportPayload\n return [\n 'team_id' => $validatedData['team_id'],\n 'group_ids' => $validatedData['groups'] ?? [],\n 'report_type' => $validatedData['type'],\n 'from_date' => $fromDate->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->format(DateTimeInterface::RFC3339),\n 'call_deal_stage' => $validatedData['deal_at_call_stages'] ?? [],\n 'current_deal_stage' => $validatedData['current_deal_stages'] ?? [],\n 'deal_min_value' => $validatedData['deal_value_min'] ?? null,\n 'deal_max_value' => $validatedData['deal_value_max'] ?? null,\n 'call_types' => $validatedData['call_types'],\n 'call_duration_min_seconds' => $validatedData['call_duration_min'] ?? null,\n 'call_duration_max_seconds' => $validatedData['call_duration_max'] ?? null,\n 'special_requirements' => $validatedData['additional_prompt_input'] ?? null,\n 'playbook_categories' => $validatedData['playbook_categories'] ?? [],\n 'request_id' => null,\n 'callback_url' => null,\n ];\n }\n\n public function shouldSendReport(array $users, ?CarbonInterface $generatedAt = null): bool\n {\n if (empty($users)) {\n return false;\n }\n\n $earliestTz = collect($users)\n ->mapWithKeys(function (array $user) {\n $tz = new DateTimeZone($user['timezone']);\n $nowUtc = new DateTime('now', new DateTimeZone('UTC'));\n $offset = $tz->getOffset($nowUtc);\n\n return [$user['timezone'] => $offset];\n })\n ->sortDesc()\n ->keys()\n ->first();\n\n $now = Carbon::now($earliestTz);\n $isScheduledTime = (int) $now->format('H') === self::SENT_REPORT_AT_HOURS;\n\n if ($isScheduledTime) {\n return true;\n }\n\n return $this->hasPassedScheduledTime($generatedAt, $earliestTz);\n }\n\n public function hasPassedScheduledTime(?CarbonInterface $generatedAt, string $timezone): bool\n {\n if ($generatedAt === null) {\n return false;\n }\n\n $now = Carbon::now($timezone);\n $scheduledTime = $now->copy()->setTime(self::SENT_REPORT_AT_HOURS, 0, 0);\n\n if ($now->hour < self::SENT_REPORT_AT_HOURS) {\n $scheduledTime = $scheduledTime->subDay();\n }\n\n $scheduledTimeUtc = $scheduledTime->copy()->utc();\n $generatedAtUtc = $generatedAt->copy()->utc();\n $nowUtc = $now->copy()->utc();\n\n return $generatedAtUtc->lt($scheduledTimeUtc) && $nowUtc->gt($scheduledTimeUtc);\n }\n\n public function calculateFromAndToDatePeriod(\n string $frequency,\n ?Carbon $fromDate = null,\n ?Carbon $toDate = null,\n DateTimeZone|string|null $timezone = 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($timezone);\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()->subWeek()->startOfWeek(CarbonInterface::MONDAY),\n 'toDate' => $now->copy()->subWeek()->endOfWeek(CarbonInterface::SUNDAY),\n ],\n self::FREQUENCY_MONTHLY => [\n 'fromDate' => $now->copy()->subMonthNoOverflow()->startOfMonth(),\n 'toDate' => $now->copy()->subMonthNoOverflow()->endOfMonth(),\n ],\n self::FREQUENCY_QUARTERLY => [\n 'fromDate' => $now->copy()->subQuarterNoOverflow()->startOfQuarter(),\n 'toDate' => $now->copy()->subQuarterNoOverflow()->endOfQuarter(),\n ],\n default => throw new InvalidArgumentException(\"Unsupported frequency: {$frequency}\"),\n };\n }\n\n private function calculateFromAndToDate(AutomatedReport $automatedReport): array\n {\n return $this->calculateFromAndToDatePeriod(\n $automatedReport->getFrequency(),\n $automatedReport->getFrom(),\n $automatedReport->getTo()\n );\n }\n\n public function getAskJiminnyGenerateReportPayload(\n AutomatedReport $automatedReport,\n AutomatedReportResult $reportResult,\n array $activityIds,\n ): array {\n return [\n 'user_question' => $automatedReport->getAskAnythingPrompt()?->getContent(),\n 'call_ids' => array_map('strval', $activityIds),\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResult->getUuid(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->getReportPeriodName($reportResult),\n 'report_name' => $automatedReport->getCustomName(),\n ];\n }\n\n private function getCallbackUrl(): string\n {\n return $this->webhookService->route('jiminny.webhook.reports.ready');\n }\n\n /**\n * Validate and transform payload data for automated reports\n *\n * @param array $data\n *\n * @throws InvalidArgumentException\n *\n * @return array\n */\n private function validateAndTransformData(array $data): array\n {\n // Validate organization (team) and check feature\n $team = $this->validateOrganization($data['organization'] ?? null);\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $type = $this->validateReportType($data['report_type'] ?? null);\n $frequency = $this->validateFrequency($data['frequency'] ?? null);\n $additionalPromptInput = $this->validateAdditionalPromptInput(\n $data['additional_prompt_input'] ?? null\n );\n $customReportName = $this->validateCustomReportName($data['custom_name'] ?? null);\n\n // Prepare data for the database\n $reportData = [\n 'team_id' => $team->getId(),\n 'type' => $type,\n 'status' => $status,\n 'frequency' => $frequency,\n 'additional_prompt_input' => $additionalPromptInput,\n 'custom_name' => $customReportName,\n ];\n\n // Validate deal values\n $reportData = $this->validateDealValues($data, $reportData);\n\n // Validate date range\n $reportData = $this->validateDateRange($data, $reportData, $frequency);\n\n // Validate call durations\n $reportData = $this->validateCallDurations($data, $reportData);\n\n // Validate call types\n $reportData = $this->validateCallTypes($data, $reportData);\n\n // Validate media types\n $reportData = $this->validateMediaTypes($data, $reportData);\n\n // Validate groups\n if (isset($data['teams'])) {\n $reportData['groups'] = $this->validateAndGetGroupIds($team, $data['teams']);\n }\n\n // Validate deal stages\n $reportData = $this->validateDealStages($data, $reportData, $team, $type);\n\n // Validate playbook categories\n $reportData = $this->validatePlaybookCategories($data, $reportData, $team);\n\n // Validate recipients\n $reportData['recipients'] = [\n 'users' => $this->validateAndGetUserIdsByTeam($team, $data['recipients'] ?? []),\n ];\n\n if (isset($data['jiminny_recipients'])) {\n // Validate Jiminny recipients\n $reportData['jiminny_recipients'] = [\n 'users' => $this->validateAndGetJiminnyUserIds((array) $data['jiminny_recipients']),\n ];\n }\n\n return $reportData;\n }\n\n private function validateDealValues(array $data, array $reportData): array\n {\n if (isset($data['min_deal_value'])) {\n $reportData['deal_value_min'] = (int) $data['min_deal_value'];\n\n if ($reportData['deal_value_min'] > 4294967295 || $reportData['deal_value_min'] < 0) {\n throw new InvalidArgumentException('Min deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_deal_value'])) {\n $reportData['deal_value_max'] = (int) $data['max_deal_value'];\n\n if ($reportData['deal_value_max'] > 4294967295 || $reportData['deal_value_max'] < 0) {\n throw new InvalidArgumentException('Max deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['min_deal_value'], $data['max_deal_value'])\n && $data['min_deal_value'] > $data['max_deal_value']\n ) {\n throw new InvalidArgumentException('Min deal value cannot be greater than max deal value');\n }\n\n return $reportData;\n }\n\n private function validateDateRange(array $data, array $reportData, string $frequency): array\n {\n // Set date range only for one_off frequency\n if ($frequency === 'one_off') {\n if (isset($data['start_date_period'])) {\n $reportData['from'] = $this->parseDate($data['start_date_period']);\n }\n\n if (isset($data['end_date_period'])) {\n $reportData['to'] = $this->parseDate($data['end_date_period']);\n }\n\n if (empty($reportData['from']) || empty($reportData['to'])) {\n throw new InvalidArgumentException(\n 'Start date and end date are required for one_off frequency'\n );\n }\n } else {\n $reportData['from'] = null;\n $reportData['to'] = null;\n }\n\n return $reportData;\n }\n\n private function validateCallDurations(array $data, array $reportData): array\n {\n // Convert call durations from minutes to seconds\n if (isset($data['min_call_duration'])) {\n $reportData['call_duration_min'] = (int) $data['min_call_duration'] * 60;\n\n if ($reportData['call_duration_min'] > 4294967295 || $reportData['call_duration_min'] < 0) {\n throw new InvalidArgumentException('Min call duration should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_call_duration'])) {\n $reportData['call_duration_max'] = (int) $data['max_call_duration'] * 60;\n\n if ($reportData['call_duration_max'] > 4294967295 || $reportData['call_duration_max'] < 0) {\n throw new InvalidArgumentException('Max call duration should be between 0 and 4294967295');\n }\n }\n\n return $reportData;\n }\n\n private function validateCallTypes(array $data, array $reportData): array\n {\n // Set call types\n $reportData['call_types'] = $data['call_type'] ?? [];\n if (empty($reportData['call_types'])) {\n $reportData['call_types'] = self::getCallTypes();\n }\n\n foreach ($reportData['call_types'] as $callType) {\n if (! in_array($callType, self::getCallTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Call type %s is invalid', $callType));\n }\n }\n\n return $reportData;\n }\n\n private function validateMediaTypes(array $data, array $reportData): array\n {\n // Set media types from input data\n $reportData['media_types'] = $data['media_types'] ?? [];\n\n if (empty($reportData['media_types'])) {\n throw new InvalidArgumentException('Media types are required');\n }\n\n foreach ($reportData['media_types'] as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n throw new InvalidArgumentException(sprintf('Media type %s is invalid', $mediaType));\n }\n }\n\n return $reportData;\n }\n\n private function validateDealStages(array $data, array $reportData, Team $team, string $reportType): array\n {\n // Validate and set deal stages\n if (isset($data['deal_stage_at_call'])) {\n $reportData['deal_at_call_stages'] =\n $this->validateAndGetDealStageIds($team, $data['deal_stage_at_call'], 'Deal stage at call');\n }\n\n if (isset($data['current_deal_stage'])) {\n $reportData['current_deal_stages'] =\n $this->validateAndGetDealStageIds($team, $data['current_deal_stage'], 'Current deal stage');\n }\n\n // Ensure current_deal_stage is not provided for loss_analysis report type\n if ($reportType === self::TYPE_LOSS_ANALYSIS && ! empty($data['current_deal_stage'])) {\n throw new InvalidArgumentException('Current deal stage is not applicable for Loss Analysis reports');\n }\n\n return $reportData;\n }\n\n // transform uuid to id\n private function validatePlaybookCategories(array $data, array $reportData, Team $team): array\n {\n $key = 'playbook_categories';\n\n if (isset($data[$key])) {\n $payloadIds = $data[$key];\n $ids = [];\n\n foreach ($payloadIds as $uuid) {\n $uuid = (string) $uuid;\n\n try {\n $playbookCategory = $this->playbookCategoryRepository->findByUuid($uuid);\n } catch (Throwable $throwable) {\n Log::error(__METHOD__ . ' ' . $throwable->getMessage());\n\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory) {\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory->hasPlaybook()) {\n throw new InvalidArgumentException(sprintf('Playbook category %s has no playbook', $uuid));\n }\n\n if ($playbookCategory->getPlaybook()->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Playbook category %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $ids[] = $playbookCategory->getId();\n }\n\n $reportData[$key] = $ids;\n }\n\n return $reportData;\n }\n\n private function validateReportStatus($status): bool\n {\n if (! in_array($status, [true, false], true)) {\n throw new InvalidArgumentException('Report status is invalid');\n }\n\n return $status;\n }\n\n private function validateReportType($type): string\n {\n if (! in_array($type, self::getTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Report type is invalid: %s', $type));\n }\n\n return $type;\n }\n\n private function validateFrequency($frequency): string\n {\n if (! in_array($frequency, self::getFrequencies(), true)) {\n throw new InvalidArgumentException('Frequency is invalid');\n }\n\n return $frequency;\n }\n\n private function validateAdditionalPromptInput(?string $additionalPromptInput): ?string\n {\n if ($additionalPromptInput && strlen($additionalPromptInput) > 5000) {\n throw new InvalidArgumentException('Additional Prompt Input should be less than 5000 characters');\n }\n\n return $additionalPromptInput;\n }\n\n private function validateCustomReportName(?string $customReportName): ?string\n {\n if ($customReportName === null || $customReportName === '') {\n return null;\n }\n\n if (strlen($customReportName) > 70) {\n throw new InvalidArgumentException('Custom report name should be less than 70 characters');\n }\n\n return $customReportName;\n }\n\n private function validateOrganization(?string $organizationUuid): Team\n {\n if (! $organizationUuid) {\n throw new InvalidArgumentException('Organization is required');\n }\n\n $team = $this->teamRepository->idOrUuid($organizationUuid);\n\n if (! $team) {\n throw new InvalidArgumentException('Organization not found');\n }\n\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n throw new InvalidArgumentException('Organization does not have the Automated Reports feature');\n }\n\n return $team;\n }\n\n private function validateAndGetGroupIds(Team $team, array $teamUuids): array\n {\n $groupIds = [];\n\n foreach ($teamUuids as $uuid) {\n $group = $this->groupRepository->findByUuid($uuid);\n\n if ($group === null || $group->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Group %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $groupIds[] = $group->getId();\n\n }\n\n return $groupIds;\n }\n\n private function validateAndGetDealStageIds(Team $team, array $stageUuids, string $propertyLabel): array\n {\n $stageIds = [];\n\n foreach ($stageUuids as $uuid) {\n $stage = $this->stageRepository->findByUuid($uuid);\n\n if ($stage === null || $stage->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Stage %s not found for team %s for %s', $uuid, $team->getUuid(), $propertyLabel)\n );\n }\n\n $stageIds[] = $stage->getId();\n }\n\n return $stageIds;\n }\n\n private function validateAndGetUserIds(array $userUuids, callable $teamCheck): array\n {\n if (empty($userUuids)) {\n return [];\n }\n\n $userIds = [];\n\n foreach ($userUuids as $uuid) {\n $user = $this->userRepository->findByUuid($uuid);\n\n if (! $user || ! $user->isStatusActive()) {\n throw new InvalidArgumentException(\n sprintf('User %s not found or is not active', $uuid)\n );\n }\n\n if (! $teamCheck($user)) {\n throw new InvalidArgumentException(\n sprintf('User %s does not belong to the allowed team(s)', $uuid)\n );\n }\n\n $userIds[] = $user->getId();\n }\n\n return $userIds;\n }\n\n private function validateAndGetUserIdsByTeam(Team $team, array $userUuids): array\n {\n return $this->validateAndGetUserIds($userUuids, fn ($user) => $user->getTeamId() === $team->getId());\n }\n\n private function validateAndGetJiminnyUserIds(array $userUuids): array\n {\n $allowedTeamIds = config('kiosk.teamIds', []);\n\n return $this->validateAndGetUserIds($userUuids, fn ($user) => in_array($user->getTeamId(), $allowedTeamIds, true));\n }\n\n private function parseDate(string $dateString): string\n {\n return date('Y-m-d H:i:s', strtotime($dateString));\n }\n\n private function generateReportResultViewUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.view', ['uuid' => $result->getUuid()]);\n }\n\n private function generateReportResultDownloadUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.download', ['uuid' => $result->getUuid()]);\n }\n\n private function getReportMediaRouteResource(AutomatedReportResult $result): string\n {\n if ($result->getMediaType() === self::MEDIA_TYPE_PDF) {\n return self::PDF_KEY;\n } elseif ($result->getMediaType() === self::MEDIA_TYPE_PODCAST) {\n return self::AUDIO_KEY;\n }\n\n throw new \\InvalidArgumentException('Unknown media type.');\n }\n\n public function getMediaPath(AutomatedReportResult $result): ?string\n {\n $url = match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => $result->getPdfUrl(),\n self::MEDIA_TYPE_PODCAST => $result->getPodcastAudioUrl(),\n default => null,\n };\n\n if ($url === null) {\n return null;\n }\n\n $path = parse_url(trim($url, '\"\\''), PHP_URL_PATH);\n\n return $path ?: null;\n }\n\n public function getFilenameSuffix(AutomatedReportResult $result): ?string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => 'Podcast',\n default => null,\n };\n }\n\n public function getMailSubjectSuffix(AutomatedReportResult $result): string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => 'report',\n self::MEDIA_TYPE_PODCAST => 'podcast',\n default => '',\n };\n }\n\n public function getMediaTypeMetadata(AutomatedReportResult $result): array\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => ['extension' => 'mp3', 'mime' => 'audio/mpeg'],\n self::MEDIA_TYPE_PDF => ['extension' => 'pdf', 'mime' => 'application/pdf'],\n default => ['extension' => null, 'mime' => null],\n };\n }\n\n public function deleteS3Files(AutomatedReportResult $result): void\n {\n $teamUuid = $result->getReport()->getTeam()->getUuid();\n $reportUuid = $result->getUuid();\n\n // delete all files for a report uuid no mather of pdf, podcast, or both\n // in case of both - the podcast files are linked to the pdf (parent) uuid\n // pdf and podcast date times should be close\n $path = sprintf('%s/%s/%s', $teamUuid, self::S3_DIR, $reportUuid);\n\n foreach (self::FILE_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted S3 file', [\n 'path' => $file,\n ]);\n }\n }\n\n foreach (self::FILE_PODCAST_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '_podcast.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted Podcast S3 file', [\n 'path' => $file,\n ]);\n }\n }\n }\n\n /**\n *\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): Collection\n {\n return $this->automatedReportsRepository->getTeamIdsWithReportsResults($teamId);\n }\n\n /**\n * Core delete logic for report results using a query\n *\n * @param Builder $query\n * @param array $logContext\n *\n * @return int\n */\n private function deleteReportResultsByQuery(Builder $query, array $logContext = []): int\n {\n $deletedCount = 0;\n\n if ($query->exists()) {\n Log::info(\n 'Run delete report results',\n array_merge(\n $logContext,\n [\n 'service' => 'AutomatedReportsService',\n ]\n )\n );\n\n $query->chunkById(50, function ($results) use (&$deletedCount, $logContext) {\n foreach ($results as $result) {\n $this->deleteReportResult($result);\n $deletedCount++;\n\n Log::info(\n 'Deleted a report result',\n array_merge(\n $logContext,\n [\n 'result_id' => $result->getId(),\n 'report_id' => $result->getReportId(),\n ]\n )\n );\n }\n });\n }\n\n return $deletedCount;\n }\n\n /**\n * Delete report results for a team by retention period\n *\n * @param Team $team\n * @param CarbonImmutable $retentionDate\n *\n * @return int Number of deleted report results\n */\n public function deleteReportsResultsInRetentionPeriod(Team $team, CarbonImmutable $retentionDate): int\n {\n $reportIds = $this->automatedReportsRepository->getReportIdsByTeam($team);\n\n if ($reportIds->isEmpty()) {\n return 0;\n }\n\n $query = $this->automatedReportsRepository\n ->getReportResultsQueryForRetention($team, $retentionDate);\n\n return $this->deleteReportResultsByQuery($query, [\n 'team_id' => $team->getId(),\n 'retention_date' => $retentionDate->toDateTimeString(),\n ]);\n }\n\n /**\n * Delete ALL report results for a specific automated report\n *\n * @param string $uuid\n *\n * @return int\n */\n public function deleteReportResults(string $uuid): int\n {\n $report = $this->getReport($uuid);\n\n $query = $this->automatedReportsRepository->getResultsByReportQuery($report);\n\n return $this->deleteReportResultsByQuery($query, [\n 'report_uuid' => $uuid,\n 'report_id' => $report->getId(),\n ]);\n }\n\n public function deleteReportResult(AutomatedReportResult $result): void\n {\n $this->deleteS3Files($result);\n\n $result->delete();\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getTeamReports(Team $team): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getReportsByTeam($team);\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getReportResults(AutomatedReport $report): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getResultsByReport($report);\n }\n\n public function deleteAllReportResults(AutomatedReport $report): void\n {\n $results = $this->getReportResults($report);\n\n /** @var AutomatedReportResult $result */\n foreach ($results as $result) {\n Log::info('Deleting result', [\n 'report' => $report->getId(),\n 'result' => $result->getId(),\n ]);\n\n $this->deleteReportResult($result);\n }\n }\n\n public function deleteAllData(Team $team): void\n {\n Log::info('Deleting automated report and results for team', [\n 'team' => $team->getId(),\n ]);\n\n $reports = $this->getTeamReports($team);\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n Log::info('Deleting report', [\n 'team' => $team->getId(),\n 'report' => $report->getId(),\n ]);\n\n $this->deleteAllReportResults($report);\n\n $report->delete();\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Carbon\\CarbonImmutable;\nuse Carbon\\CarbonInterface;\nuse Carbon\\Exceptions\\InvalidFormatException;\nuse DateTime;\nuse DateTimeInterface;\nuse DateTimeZone;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\InputTypeEnum;\nuse Jiminny\\Component\\AskAnything\\AskAnythingPromptService;\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Contracts\\Repositories\\PlaybookCategoryRepository;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\ModelNotFoundException;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Partner;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Repositories\\GroupRepository;\nuse Jiminny\\Repositories\\SearchRepository;\nuse Jiminny\\Repositories\\StageRepository;\nuse Throwable;\n\nclass AutomatedReportsService\n{\n public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';\n public const string TYPE_ASK_JIMINNY = 'ask_jiminny';\n\n /**\n * Standard report types (used by kiosk for existing automated reports).\n */\n // @TODO this will add filter, however if we need to control feature by FF we need conditional logic\n public const array TYPES = [\n ['id' => 'exec_summary', 'name' => 'Exec Summary'],\n ['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],\n ['id' => 'product_feedback', 'name' => 'Product Feedback'],\n ['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],\n// ['id' => 'questions', 'name' => 'Questions'],\n// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],\n ];\n\n public const array ALL_TYPES = [\n ...self::TYPES,\n ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],\n ];\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\n /**\n * Frequencies for standard (non-Ask Jiminny) reports.\n */\n public const array FREQUENCIES = [\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n\n /**\n * Frequencies for Ask Jiminny reports.\n */\n public const array ASK_JIMINNY_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ];\n\n public const string MEDIA_TYPE_PDF = 'pdf';\n public const string MEDIA_TYPE_PODCAST = 'podcast';\n public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];\n public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];\n public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];\n public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];\n\n public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];\n public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];\n public const int SENT_REPORT_AT_HOURS = 5;\n public const string PDF_KEY = 'pdf';\n public const string AUDIO_KEY = 'audio';\n\n private const array ALL_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n private const string S3_DIR = 'reports';\n private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];\n private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];\n\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly GroupRepository $groupRepository,\n private readonly UserRepository $userRepository,\n private readonly StageRepository $stageRepository,\n private readonly DealStagesService $dealStagesService,\n private readonly RecipientsService $recipientsService,\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly Webhook $webhookService,\n private readonly BusDispatcher $dispatcher,\n private readonly ActivityTypeService $activityTypeService,\n private readonly PlaybookCategoryRepository $playbookCategoryRepository,\n private readonly AskAnythingPromptService $askAnythingPromptService,\n private readonly SearchRepository $activitySearchRepository,\n private readonly AskAnythingRepository $askAnythingRepository,\n ) {\n }\n\n public static function getTypes(): array\n {\n $types = self::TYPES;\n\n return array_map(static function ($type) {\n return $type['id'];\n }, $types);\n }\n\n public static function getCallTypes(): array\n {\n return array_map(static function ($callType) {\n return $callType['id'];\n }, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);\n }\n\n public static function getFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::FREQUENCIES);\n }\n\n // front-facing structure\n public function getReportEnabledFieldData(bool $value = false): array\n {\n return [\n 'id' => 'report_enabled',\n 'label' => '',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'value' => $value,\n ];\n }\n\n // Organizations = Teams\n public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array\n {\n $options = $this->getTeams(partner: $partner);\n\n if ($shortVersion) {\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value,\n 'dependencies' => [\n 'teams',\n 'deal_stage_at_call',\n 'current_deal_stage',\n 'recipients',\n ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,\n ],\n 'dependsOn' => [],\n ];\n }\n\n // Teams = Groups\n public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array\n {\n if ($shortVersion) {\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value, // value should be an array of objects {id, name}\n 'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],\n 'dependsOn' => [],\n ];\n }\n\n public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array\n {\n $types = [];\n if ($team instanceof Team) {\n if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n $types = self::TYPES;\n }\n if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {\n $types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];\n }\n } else {\n $types = self::TYPES;\n }\n\n if ($shortVersion) {\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'options' => $types,\n ];\n }\n\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $types,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getFrequencyFieldData(?string $value = null): array\n {\n return [\n 'id' => 'frequency',\n 'label' => 'Frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::FREQUENCIES,\n 'value' => $value,\n 'dependencies' => ['period'],\n 'dependsOn' => [],\n ];\n }\n\n public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array\n {\n return [\n 'id' => 'period',\n 'label' => 'Select one-off period',\n 'inputType' => InputTypeEnum::DATE_RANGE,\n 'required' => true,\n 'placeholder' => 'Select',\n 'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],\n 'queryParams' => [\n 'startDate' => 'start_date_period',\n 'endDate' => 'end_date_period',\n ],\n 'dependencies' => [],\n 'dependsOn' => ['frequency'],\n ];\n }\n\n public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array\n {\n return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);\n }\n\n public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);\n }\n\n public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);\n }\n\n public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'deal_value',\n 'label' => 'Deal Value',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_deal_value',\n 'max' => 'max_deal_value',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array\n {\n $value = [];\n $conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;\n $dialerOn && $value[] = self::CALL_TYPE_DIALER;\n\n return [\n 'id' => 'call_type',\n 'label' => 'Call Type',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => [\n self::CALL_TYPE_CONFERENCE,\n self::CALL_TYPE_DIALER,\n ],\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getMediaTypeFieldData(?AutomatedReport $report = null): array\n {\n $value = [];\n\n if ($report) {\n $value = $this->transformMediaTypes($report);\n }\n\n return [\n 'id' => 'media_types',\n 'label' => 'Export as',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => self::MEDIA_TYPE_OBJECTS,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'call_duration',\n 'label' => 'Call Duration',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_call_duration',\n 'max' => 'max_call_duration',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getRecipientsFieldData(?Team $team = null, array $value = []): array\n {\n return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);\n }\n\n public function getJiminnyRecipientsFieldData(array $value = []): array\n {\n return $this->recipientsService->getJiminnyRecipientsFieldData($value);\n }\n\n public function getAdditionalPromptInputFieldData(?string $value = null): array\n {\n return [\n 'id' => 'additional_prompt_input',\n 'label' => 'Special requirements',\n 'inputType' => InputTypeEnum::TEXTAREA,\n 'required' => false,\n 'placeholder' => 'What should be the focus of the report?',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCustomReportNameFieldData(?string $value = null): array\n {\n return [\n 'id' => 'custom_name',\n 'label' => 'Custom report name',\n 'inputType' => InputTypeEnum::TEXT,\n 'required' => false,\n 'placeholder' => 'Enter custom name',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n // data providers\n public function getTeams(?Partner $partner = null): array\n {\n $teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);\n\n $teamData = [];\n foreach ($teams as $team) {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n continue;\n }\n\n $teamData[] = $this->transformTeam($team);\n }\n\n return $teamData;\n }\n\n public function getTeamGroups(string $teamUuid): array\n {\n $data = [];\n $team = $this->getTeam($teamUuid);\n\n if ($team !== null) {\n $groups = $team->groups()->get();\n\n foreach ($groups as $group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array\n {\n $data = [];\n $teams = $this->getTeams(partner: $partner);\n\n foreach ($teams as $team) {\n if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {\n continue;\n }\n\n $data[] = [\n 'label' => $team['name'],\n 'groups' => $this->getTeamGroups($team['id']),\n ];\n }\n\n return $data;\n }\n\n public function getTeam(string $teamUuid): ?Team\n {\n return $this->teamRepository->idOrUuid($teamUuid);\n }\n\n public function getTeamById(int $teamId): ?Team\n {\n return $this->teamRepository->find($teamId);\n }\n\n public function getGroupsUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportGroups = $report->getGroups();\n foreach ($reportGroups as $groupId) {\n if ($group = $this->groupRepository->find($groupId)) {\n $uuids[] = $group->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getPlaybookCategoriesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $playbookCategories = $report->getPlaybookCategories();\n foreach ($playbookCategories as $id) {\n if ($category = $this->playbookCategoryRepository->find($id)) {\n $uuids[] = $category->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getDealAtCallStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getDealAtCallStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getCurrentDealStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getCurrentDealStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getRecipients());\n }\n\n public function getJiminnyUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function extractUserUuids(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => $user->getUuid())\n ->values()\n ->all();\n }\n\n // get mail data\n public function getRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getRecipients());\n }\n\n /**\n * @return array<UserContract>\n */\n public function getRecipientUserObjects(AutomatedReport $report): array\n {\n $userIds = $report->getRecipients()['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->values()\n ->all();\n }\n\n private function getJiminnyRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function buildRecipientUsers(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => [\n 'email' => $user->getEmailAddress(),\n 'name' => $user->getName(),\n 'timezone' => $user->getTimezone()->getName(),\n ])\n ->values()\n ->all();\n }\n\n public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array\n {\n if ($report->isAskJiminnyReport()) {\n $recipients = $this->resolveAskJiminnyRecipients($report);\n } else {\n $recipients = $this->getRecipientUsers($report);\n if ($includeJiminny) {\n $recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));\n }\n }\n\n $emails = [];\n\n return array_values(array_filter(\n $recipients,\n static function ($recipient) use (&$emails) {\n if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {\n return false;\n }\n $emails[] = $recipient['email'];\n\n return true;\n }\n ));\n }\n\n private function resolveAskJiminnyRecipients(AutomatedReport $report): array\n {\n $recipients = [];\n\n $creator = $report->getCreator();\n if ($creator !== null) {\n $recipients[] = [\n 'email' => $creator->getEmailAddress(),\n 'name' => $creator->getName(),\n 'timezone' => $creator->getTimezone()->getName(),\n ];\n }\n\n return array_merge(\n $recipients,\n $this->buildRecipientUsers($report->getRecipients()),\n $this->getGroupRecipientUsers($report),\n );\n }\n\n private function getGroupRecipientUsers(AutomatedReport $report): array\n {\n $users = [];\n foreach ($report->getGroups() as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group === null) {\n continue;\n }\n foreach ($group->getMembers() as $member) {\n $users[] = [\n 'email' => $member->getEmailAddress(),\n 'name' => $member->getName(),\n 'timezone' => $member->getTimezone()->getName(),\n ];\n }\n }\n\n return $users;\n }\n\n public function getReportTypeName(AutomatedReportResult $report): string\n {\n $type = $report->getReport()->getType();\n\n $getType = $this->transformReportType($type);\n\n return $getType['name'];\n }\n\n public function getReportPeriodName(AutomatedReportResult $report): string\n {\n $from = $report->getFromDate();\n $to = $report->getToDate();\n $frequency = $report->getReport()->getFrequency();\n\n if ($from === null || $to === null) {\n if (! $report->getReport()->isAskJiminnyReport()) {\n $invalidPeriod = $from === null ? 'from' : 'to';\n\n throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);\n }\n\n $timezone = $report->getReport()->getCreator()?->getTimezone();\n $period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);\n $from = $period['fromDate'];\n $to = $period['toDate'];\n }\n\n return $this->formatReportPeriodName($frequency, $from, $to);\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 getReportTeamsName(AutomatedReportResult $report): string\n {\n $groups = $report->getGroups();\n\n if (empty($groups)) {\n return 'All';\n }\n\n // Get group names from repository\n $groupNames = [];\n foreach ($groups as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group) {\n $groupNames[] = $group->getName();\n }\n }\n\n if (count($groupNames) === 1) {\n // Single team format\n $teamsName = $groupNames[0];\n } else {\n // Multiple teams format\n $teamsName = implode(', ', $groupNames);\n }\n\n return $teamsName;\n }\n\n public function getReportFileName(AutomatedReportResult $report): string\n {\n $customName = $report->getReport()->getCustomName();\n $periodName = $this->getReportPeriodName($report);\n $filenameSuffix = $this->getFilenameSuffix($report);\n\n if ($customName) {\n if ($filenameSuffix) {\n $customName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$customName} - {$periodName}\");\n }\n\n $baseName = $this->getReportTypeName($report);\n\n if ($filenameSuffix) {\n $baseName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}\");\n }\n\n public function getReportFileNameWithExtension(AutomatedReportResult $result): string\n {\n $extension = $this->getMediaTypeMetadata($result)['extension'];\n\n return $this->getReportFileName($result) . '.' . $extension;\n }\n\n public function sanitizeFileName(string $fileName): string\n {\n return str_replace(['/', '\\\\'], '-', $fileName);\n }\n\n public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool\n {\n $recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);\n if (in_array($user->getId(), $recipientIds, true)) {\n return true;\n }\n\n if ($report->isAskJiminnyReport()) {\n $groupId = $user->getGroupId();\n if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {\n return true;\n }\n }\n\n return false;\n }\n\n public function transformReportResults(Collection $automatedReportResults): array\n {\n $data = [];\n foreach ($automatedReportResults as $automatedReportResult) {\n /** @var AutomatedReportResult $automatedReportResult */\n\n $report = $automatedReportResult->getReport();\n\n $createdBy = $report->getCreator();\n $creator = [\n 'id' => $createdBy?->getUuid(),\n 'name' => $createdBy?->getName(),\n 'email' => $createdBy?->getEmailAddress(),\n 'photoUrl' => $createdBy?->getPhotoUrl(),\n ];\n\n $data[] = [\n 'id' => $automatedReportResult->getUuid(),\n 'name' => $automatedReportResult->getName(),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n 'recipients' => $this->buildRecipients($report),\n 'report_type' => $this->transformReportType($report->getType()),\n 'media_type' => $automatedReportResult->getMediaType(),\n 'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),\n 'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),\n 'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),\n 'creator' => $creator,\n ];\n }\n\n return $data;\n }\n\n private function buildRecipients(AutomatedReport $report): array\n {\n $creatorUuid = $report->getCreator()?->getUuid();\n\n $recipients = array_values(array_filter(\n $this->transformRecipients($report->getRecipients()),\n static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,\n ));\n\n if (! $report->isAskJiminnyReport()) {\n return $recipients;\n }\n\n return [\n ...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),\n ...$recipients,\n ];\n }\n\n public function hasCallTypeConference(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);\n }\n\n public function hasCallTypeDialer(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);\n }\n\n // transformers\n private function transformTeam(Team $team): array\n {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n return [];\n }\n\n return [\n 'id' => $team->getUuid(),\n 'name' => $team->getName(),\n ];\n }\n\n private function transformReportFullView(AutomatedReport $report): array\n {\n $base = $this->transformReportBase($report);\n\n return $report->getType() === self::TYPE_ASK_JIMINNY\n ? $base + $this->transformAskJiminnyFields($report)\n : $base + $this->transformStandardReportFields($report);\n }\n\n private function transformReportBase(AutomatedReport $report): array\n {\n return [\n 'id' => $report->getUuid(),\n 'organization' => $this->transformOrganization(team: $report->getTeam()),\n 'report_type' => $this->transformReportType($report->getType()),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n ];\n }\n\n private function transformStandardReportFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n\n return [\n 'report_enabled' => $report->getStatus(),\n 'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),\n 'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),\n 'deal_value_min' => $report->getDealValueMin(),\n 'deal_value_max' => $report->getDealValueMax(),\n 'call_types' => $this->transformCallType($report->getCallTypes()),\n 'media_types' => $this->transformMediaTypes($report),\n 'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),\n 'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),\n 'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),\n 'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),\n 'recipients' => $this->transformRecipients($report->getRecipients()),\n 'created_by' => $this->transformCreator($report->getCreator()),\n 'additional_prompt_input' => $report->getAdditionalPromptInput(),\n 'custom_name' => $report->getCustomName(),\n 'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),\n 'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),\n 'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),\n ];\n }\n\n private function transformAskJiminnyFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n $creatorId = $report->getAttribute('created_by');\n $explicitUserIds = array_values(array_filter(\n $report->getRecipients()['users'] ?? [],\n static fn ($id) => $id !== $creatorId\n ));\n\n return [\n 'report_name' => $report->getCustomName(),\n 'enabled' => $report->getStatus(),\n 'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),\n 'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),\n 'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),\n 'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),\n ];\n }\n\n private function transformOrganization(?Team $team): array\n {\n return [\n 'id' => $team?->getUuid(),\n 'name' => $team?->getName(),\n ];\n }\n\n private function transformReportType(string $type): array\n {\n foreach (self::ALL_TYPES as $typeItem) {\n if ($typeItem['id'] === $type) {\n return $typeItem;\n }\n }\n\n return [];\n }\n\n private function transformCallType(array $types): array\n {\n $result = [];\n $callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];\n\n foreach ($types as $type) {\n foreach ($callTypes as $callTypeItem) {\n if ($callTypeItem['id'] === $type) {\n $result[] = $callTypeItem;\n\n break;\n }\n }\n }\n\n return $result;\n }\n\n private function transformMediaTypes(AutomatedReport $report): array\n {\n $values = [];\n\n foreach ($report->getMediaTypes() as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n continue;\n }\n\n $values[] = match ($mediaType) {\n self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,\n self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,\n };\n }\n\n return $values;\n }\n\n private function transformFrequency(string $frequency): array\n {\n foreach (self::ALL_FREQUENCIES as $frequencyItem) {\n if ($frequencyItem['id'] === $frequency) {\n return $frequencyItem;\n }\n }\n\n return [];\n }\n\n public function transformDurationToMinutes(?int $duration): ?int\n {\n if (! $duration) {\n return null;\n }\n\n return (int) ($duration / 60);\n }\n\n private function transformGroups(?Team $team, array $groupsIds): array\n {\n if (empty($groupsIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($groupsIds as $groupId) {\n $group = $team->groups()->where('id', $groupId)->first();\n\n if ($group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n 'photoUrl' => $group->getPhotoUrl(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformStages(?Team $team, array $stagesIds): array\n {\n if (empty($stagesIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($stagesIds as $stageId) {\n $stage = $team->stages()->where('id', $stageId)->first();\n\n if ($stage) {\n $data[] = [\n 'id' => $stage->getUuid(),\n 'name' => $stage->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformRecipients(array $recipients): array\n {\n $users = [];\n foreach ($recipients['users'] ?? [] as $userId) {\n $users[] = $this->transformUser($userId);\n }\n\n return $users;\n }\n\n private function transformCreator(?User $user): ?array\n {\n if ($user === null) {\n return null;\n }\n\n return $this->transformUser($user->getId());\n }\n\n private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array\n {\n if ($prompt === null) {\n return null;\n }\n\n return [\n 'id' => $prompt->getUuid(),\n 'name' => $prompt->getTitle(),\n ];\n }\n\n private function transformSafeSearch(?Search $search): ?array\n {\n if ($search === null) {\n return null;\n }\n\n return [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ];\n }\n\n private function transformUser(int $userId): array\n {\n /* @var ?User $user */\n $user = $this->userRepository->find($userId);\n\n return [\n 'id' => $user?->getUuid(),\n 'name' => $user?->getName(),\n 'email' => $user?->getEmailAddress(),\n 'photoUrl' => $user?->getPhotoUrl(),\n ];\n }\n\n public function create(array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $validatedData['created_by'] = auth()->id();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function update(string $uuid, array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $report = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $report) {\n throw new InvalidArgumentException('Report not found');\n }\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Create an Ask Jiminny report.\n */\n public function createAskJiminnyReport(array $data, User $creator): array\n {\n $validatedData = $this->validateAskJiminnyReportData($data, $creator);\n $validatedData['created_by'] = $creator->getId();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Update an Ask Jiminny report.\n */\n public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array\n {\n if (! $report->isAskJiminnyReport()) {\n throw new InvalidArgumentException('Report is not an Ask Jiminny report');\n }\n\n $validatedData = $this->validateAskJiminnyReportData($data, $user);\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array\n {\n if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {\n throw new InvalidArgumentException(\n 'This report is missing a saved search or prompt. ' .\n 'Edit the report to complete the setup before enabling it.'\n );\n }\n\n $this->automatedReportsRepository->update($report, ['status' => $status]);\n\n return $this->transformReportFullView($report->fresh());\n }\n\n /**\n * Validate and transform data for Ask Jiminny reports.\n */\n private function validateAskJiminnyReportData(array $data, User $user): array\n {\n // Validate name\n $name = trim($data['report_name'] ?? '');\n if (empty($name)) {\n throw new InvalidArgumentException('Report name is required');\n }\n if (mb_strlen($name) > 50) {\n throw new InvalidArgumentException('Report name must be 50 characters or less');\n }\n\n // Validate frequency (only daily, weekly, monthly for Ask Jiminny)\n $frequency = $data['frequency'] ?? null;\n $askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];\n if (! in_array($frequency, $askJiminnyFrequencies, true)) {\n throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');\n }\n\n // Validate expiration date\n $expiresAt = $data['expires_on'] ?? null;\n if (empty($expiresAt)) {\n throw new InvalidArgumentException('Expiration date is required');\n }\n\n try {\n $expiresAtDate = Carbon::parse($expiresAt);\n } catch (InvalidFormatException $e) {\n throw new InvalidArgumentException('Expiration date format is invalid');\n }\n $maxExpiration = Carbon::now()->addYear()->endOfDay();\n if ($expiresAtDate->gt($maxExpiration)) {\n throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');\n }\n if ($expiresAtDate->isPast()) {\n throw new InvalidArgumentException('Expiration date cannot be in the past');\n }\n\n // Validate saved search\n $activitySearchId = $data['saved_search'] ?? null;\n if (empty($activitySearchId)) {\n throw new InvalidArgumentException('Saved search is required');\n }\n $savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);\n if (! $savedSearch) {\n throw new InvalidArgumentException('Saved search not found or does not belong to you');\n }\n\n // Validate saved prompt\n $askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;\n if (empty($askAnythingPromptId)) {\n throw new InvalidArgumentException('Ask Jiminny prompt is required');\n }\n $prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);\n if (! $prompt) {\n throw new InvalidArgumentException('Ask Jiminny prompt not found');\n }\n\n // Validate status\n $status = $data['enabled'] ?? false;\n\n $recipientUserIds = [$user->getId()];\n\n if (! empty($data['share_users'])) {\n $sharedUserIds = $this->validateAndGetUserIdsByTeam(\n $user->team,\n (array) $data['share_users']\n );\n $recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);\n }\n\n $sharedGroupIds = [];\n if (! empty($data['share_teams'])) {\n $sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);\n }\n\n $recipientUserIds = array_values(array_unique($recipientUserIds));\n\n return [\n 'team_id' => $user->getTeamId(),\n 'type' => self::TYPE_ASK_JIMINNY,\n 'status' => (bool) $status,\n 'frequency' => $frequency,\n 'custom_name' => $name,\n 'activity_search_id' => $savedSearch->getId(),\n 'ask_anything_prompt_id' => $prompt->getId(),\n 'expires_at' => $expiresAtDate->toDateString(),\n 'media_types' => [self::MEDIA_TYPE_PDF],\n 'call_types' => [],\n 'recipients' => ['users' => $recipientUserIds],\n 'groups' => $sharedGroupIds,\n ];\n }\n\n public static function getAskJiminnyFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::ASK_JIMINNY_FREQUENCIES);\n }\n\n public function getAskJiminnyReportFilters(User $user): array\n {\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n return [\n [\n 'id' => 'prompt',\n 'label' => 'Prompt',\n 'options' => $prompts,\n ],\n [\n 'id' => 'saved_search',\n 'label' => 'Saved Search',\n 'options' => $savedSearches,\n ],\n ];\n }\n\n public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array\n {\n $team = $user->getTeam();\n $userTimezone = $user->getTimezone();\n\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n $teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ])->values()->all();\n\n $shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];\n\n $sharedTeamsValue = [];\n $sharedUsersValue = [];\n if ($report) {\n $sharedTeamsValue = $this->transformGroups($team, $report->getGroups());\n\n $recipientUserIds = $report->getRecipients()['users'] ?? [];\n $creatorId = $report->getAttribute('created_by');\n $sharedUserIds = array_values(array_filter(\n $recipientUserIds,\n static fn ($id) => $id !== $creatorId\n ));\n $sharedUsersValue = collect($sharedUserIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (User $u) => [\n 'id' => $u->getUuid(),\n 'name' => $u->getName(),\n ])\n ->values()\n ->all();\n }\n\n return [\n 'fields' => [\n [\n 'id' => 'enabled',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'label' => '',\n 'value' => $report?->getStatus() ?? false,\n ],\n [\n 'id' => 'report_name',\n 'inputType' => InputTypeEnum::TEXT,\n 'label' => 'Name',\n 'placeholder' => 'Enter name',\n 'required' => true,\n 'validation' => ['maxLength' => 50],\n 'value' => $report?->getCustomName() ?? '',\n ],\n [\n 'id' => 'frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Frequency',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::ASK_JIMINNY_FREQUENCIES,\n 'value' => $report ? $this->transformFrequency($report->getFrequency()) : null,\n ],\n [\n 'id' => 'expires_on',\n 'inputType' => InputTypeEnum::DATE,\n 'label' => 'Expires on',\n 'required' => true,\n 'placeholder' => 'Select',\n 'validation' => [\n 'minDate' => now($userTimezone)->toDateString(),\n 'maxDate' => now($userTimezone)->addYear()->toDateString(),\n ],\n 'value' => $report?->getExpiresAt()?->toDateString(),\n ],\n [\n 'id' => 'share_teams',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team',\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $teamGroups,\n 'value' => $sharedTeamsValue,\n ],\n [\n 'id' => 'share_users',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team member',\n 'required' => false,\n 'placeholder' => 'Select',\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'options' => $shareUsers,\n 'value' => $sharedUsersValue,\n ],\n [\n 'id' => 'saved_search',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Saved search',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $savedSearches,\n 'value' => $report && $report->getSavedSearch() ? [\n 'id' => $report->getSavedSearch()->getUuid(),\n 'name' => $report->getSavedSearch()->getName(),\n ] : null,\n ],\n [\n 'id' => 'ask_jiminny_prompt',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Ask Jiminny prompt',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $prompts,\n 'value' => $report && $report->getAskAnythingPrompt() ? [\n 'id' => $report->getAskAnythingPrompt()->getUuid(),\n 'name' => $report->getAskAnythingPrompt()->getTitle(),\n ] : null,\n ],\n ],\n ];\n }\n\n private function updateResultNames(AutomatedReport $automatedReport): void\n {\n $results = $this->automatedReportsRepository->getResultsByReport($automatedReport);\n\n foreach ($results as $result) {\n $result->update(['name' => $this->getReportFileName($result)]);\n }\n }\n\n public function updateStatus(string $uuid, array $data): array\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $automatedReport->update([\n 'status' => $status,\n ]);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n private function generateOneOffReport(AutomatedReport $automatedReport): void\n {\n // the scheduler handles all the other frequency types\n if ($automatedReport->getStatus() === false || $automatedReport->getFrequency() !== self::FREQUENCY_ONE_OFF) {\n return;\n }\n\n $this->dispatcher->dispatch(new RequestGenerateReportJob($automatedReport->getUuid()));\n }\n\n public function getReport(string $uuid, ?Partner $partner = null): AutomatedReport\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n if ($partner !== null && ! $partner->isDefaultPartner() && $automatedReport->team->partner_id !== $partner->getId()) {\n throw new ModelNotFoundException('Report not found');\n }\n\n return $automatedReport;\n }\n\n public function get(string $uuid, ?Partner $partner = null): array\n {\n $automatedReport = $this->getReport($uuid, $partner);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function list(string $sortColumn = 'created_at', string $sortDirection = 'desc', ?Partner $partner = null): array\n {\n $results = [];\n $collection = $this->automatedReportsRepository->getAllStandardReports($sortColumn, $sortDirection, $partner);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function listAskJiminnyReports(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): array {\n $results = [];\n $collection = $this->automatedReportsRepository->getAskJiminnyReportsByUser($user, $sortColumn, $sortDirection);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function delete(string $uuid): void\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $automatedReport->delete();\n }\n\n public function createReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n return $this->automatedReportsRepository->createResult(\n array_merge(\n [\n 'report_id' => $automatedReport->getId(),\n 'status' => AutomatedReportResult::STATUS_DEFAULT,\n ],\n $data\n )\n );\n }\n\n public function getOrCreateReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n $existing = $this->automatedReportsRepository->findLatestSameDayDefaultOrFailedResult($automatedReport);\n\n if ($existing !== null) {\n $existing->update(['status' => AutomatedReportResult::STATUS_DEFAULT]);\n\n return $existing;\n }\n\n return $this->createReportResult($automatedReport, $data);\n }\n\n public function getReportResult(string $resultUuid): AutomatedReportResult\n {\n $report = $this->automatedReportsRepository->findResultByUuid($resultUuid);\n\n if (! $report) {\n throw new ModelNotFoundException('Report Result not found');\n }\n\n return $report;\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return $this->automatedReportsRepository->findChildResult($result, $type);\n }\n\n // prophet API calls\n /**\n * @throws ApplicationException\n */\n public function getGenerateReportPayload(AutomatedReport $automatedReport, string $reportResultUuid): array\n {\n $period = $this->calculateFromAndToDate($automatedReport);\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n return [\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResultUuid,\n 'report_type' => $automatedReport->getType(),\n 'media_types' => $automatedReport->getMediaTypes(),\n 'from_date' => $fromDate->startOfDay()->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->endOfDay()->format(DateTimeInterface::RFC3339),\n 'group_ids' => $automatedReport->getGroups(),\n 'call_deal_stage' => $automatedReport->getDealAtCallStages(),\n 'current_deal_stage' => $automatedReport->getCurrentDealStages(),\n 'deal_min_value' => $automatedReport->getDealValueMin(),\n 'deal_max_value' => $automatedReport->getDealValueMax(),\n 'call_types' => $automatedReport->getCallTypes(),\n 'call_duration_min_seconds' => $automatedReport->getCallDurationMin(),\n 'call_duration_max_seconds' => $automatedReport->getCallDurationMax(),\n 'special_requirements' => $automatedReport->getAdditionalPromptInput(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->formatReportPeriodName(\n $automatedReport->getFrequency(),\n $fromDate,\n $toDate,\n ),\n 'playbook_categories' => $automatedReport->getPlaybookCategories(),\n 'custom_name' => $automatedReport->getCustomName(),\n ];\n }\n\n // $inputPayload - FE payload structure\n public function getActivitiesCountPayload(array $inputPayload): array\n {\n // Use validateAndTransformData to validate and normalize input\n $validatedData = $this->validateAndTransformData($inputPayload);\n $period = $this->calculateFromAndToDatePeriod(\n $validatedData['frequency'],\n Carbon::parse($validatedData['from']),\n Carbon::parse($validatedData['to']),\n );\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n // Create payload similar to getGenerateReportPayload\n return [\n 'team_id' => $validatedData['team_id'],\n 'group_ids' => $validatedData['groups'] ?? [],\n 'report_type' => $validatedData['type'],\n 'from_date' => $fromDate->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->format(DateTimeInterface::RFC3339),\n 'call_deal_stage' => $validatedData['deal_at_call_stages'] ?? [],\n 'current_deal_stage' => $validatedData['current_deal_stages'] ?? [],\n 'deal_min_value' => $validatedData['deal_value_min'] ?? null,\n 'deal_max_value' => $validatedData['deal_value_max'] ?? null,\n 'call_types' => $validatedData['call_types'],\n 'call_duration_min_seconds' => $validatedData['call_duration_min'] ?? null,\n 'call_duration_max_seconds' => $validatedData['call_duration_max'] ?? null,\n 'special_requirements' => $validatedData['additional_prompt_input'] ?? null,\n 'playbook_categories' => $validatedData['playbook_categories'] ?? [],\n 'request_id' => null,\n 'callback_url' => null,\n ];\n }\n\n public function shouldSendReport(array $users, ?CarbonInterface $generatedAt = null): bool\n {\n if (empty($users)) {\n return false;\n }\n\n $earliestTz = collect($users)\n ->mapWithKeys(function (array $user) {\n $tz = new DateTimeZone($user['timezone']);\n $nowUtc = new DateTime('now', new DateTimeZone('UTC'));\n $offset = $tz->getOffset($nowUtc);\n\n return [$user['timezone'] => $offset];\n })\n ->sortDesc()\n ->keys()\n ->first();\n\n $now = Carbon::now($earliestTz);\n $isScheduledTime = (int) $now->format('H') === self::SENT_REPORT_AT_HOURS;\n\n if ($isScheduledTime) {\n return true;\n }\n\n return $this->hasPassedScheduledTime($generatedAt, $earliestTz);\n }\n\n public function hasPassedScheduledTime(?CarbonInterface $generatedAt, string $timezone): bool\n {\n if ($generatedAt === null) {\n return false;\n }\n\n $now = Carbon::now($timezone);\n $scheduledTime = $now->copy()->setTime(self::SENT_REPORT_AT_HOURS, 0, 0);\n\n if ($now->hour < self::SENT_REPORT_AT_HOURS) {\n $scheduledTime = $scheduledTime->subDay();\n }\n\n $scheduledTimeUtc = $scheduledTime->copy()->utc();\n $generatedAtUtc = $generatedAt->copy()->utc();\n $nowUtc = $now->copy()->utc();\n\n return $generatedAtUtc->lt($scheduledTimeUtc) && $nowUtc->gt($scheduledTimeUtc);\n }\n\n public function calculateFromAndToDatePeriod(\n string $frequency,\n ?Carbon $fromDate = null,\n ?Carbon $toDate = null,\n DateTimeZone|string|null $timezone = 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($timezone);\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()->subWeek()->startOfWeek(CarbonInterface::MONDAY),\n 'toDate' => $now->copy()->subWeek()->endOfWeek(CarbonInterface::SUNDAY),\n ],\n self::FREQUENCY_MONTHLY => [\n 'fromDate' => $now->copy()->subMonthNoOverflow()->startOfMonth(),\n 'toDate' => $now->copy()->subMonthNoOverflow()->endOfMonth(),\n ],\n self::FREQUENCY_QUARTERLY => [\n 'fromDate' => $now->copy()->subQuarterNoOverflow()->startOfQuarter(),\n 'toDate' => $now->copy()->subQuarterNoOverflow()->endOfQuarter(),\n ],\n default => throw new InvalidArgumentException(\"Unsupported frequency: {$frequency}\"),\n };\n }\n\n private function calculateFromAndToDate(AutomatedReport $automatedReport): array\n {\n return $this->calculateFromAndToDatePeriod(\n $automatedReport->getFrequency(),\n $automatedReport->getFrom(),\n $automatedReport->getTo()\n );\n }\n\n public function getAskJiminnyGenerateReportPayload(\n AutomatedReport $automatedReport,\n AutomatedReportResult $reportResult,\n array $activityIds,\n ): array {\n return [\n 'user_question' => $automatedReport->getAskAnythingPrompt()?->getContent(),\n 'call_ids' => array_map('strval', $activityIds),\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResult->getUuid(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->getReportPeriodName($reportResult),\n 'report_name' => $automatedReport->getCustomName(),\n ];\n }\n\n private function getCallbackUrl(): string\n {\n return $this->webhookService->route('jiminny.webhook.reports.ready');\n }\n\n /**\n * Validate and transform payload data for automated reports\n *\n * @param array $data\n *\n * @throws InvalidArgumentException\n *\n * @return array\n */\n private function validateAndTransformData(array $data): array\n {\n // Validate organization (team) and check feature\n $team = $this->validateOrganization($data['organization'] ?? null);\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $type = $this->validateReportType($data['report_type'] ?? null);\n $frequency = $this->validateFrequency($data['frequency'] ?? null);\n $additionalPromptInput = $this->validateAdditionalPromptInput(\n $data['additional_prompt_input'] ?? null\n );\n $customReportName = $this->validateCustomReportName($data['custom_name'] ?? null);\n\n // Prepare data for the database\n $reportData = [\n 'team_id' => $team->getId(),\n 'type' => $type,\n 'status' => $status,\n 'frequency' => $frequency,\n 'additional_prompt_input' => $additionalPromptInput,\n 'custom_name' => $customReportName,\n ];\n\n // Validate deal values\n $reportData = $this->validateDealValues($data, $reportData);\n\n // Validate date range\n $reportData = $this->validateDateRange($data, $reportData, $frequency);\n\n // Validate call durations\n $reportData = $this->validateCallDurations($data, $reportData);\n\n // Validate call types\n $reportData = $this->validateCallTypes($data, $reportData);\n\n // Validate media types\n $reportData = $this->validateMediaTypes($data, $reportData);\n\n // Validate groups\n if (isset($data['teams'])) {\n $reportData['groups'] = $this->validateAndGetGroupIds($team, $data['teams']);\n }\n\n // Validate deal stages\n $reportData = $this->validateDealStages($data, $reportData, $team, $type);\n\n // Validate playbook categories\n $reportData = $this->validatePlaybookCategories($data, $reportData, $team);\n\n // Validate recipients\n $reportData['recipients'] = [\n 'users' => $this->validateAndGetUserIdsByTeam($team, $data['recipients'] ?? []),\n ];\n\n if (isset($data['jiminny_recipients'])) {\n // Validate Jiminny recipients\n $reportData['jiminny_recipients'] = [\n 'users' => $this->validateAndGetJiminnyUserIds((array) $data['jiminny_recipients']),\n ];\n }\n\n return $reportData;\n }\n\n private function validateDealValues(array $data, array $reportData): array\n {\n if (isset($data['min_deal_value'])) {\n $reportData['deal_value_min'] = (int) $data['min_deal_value'];\n\n if ($reportData['deal_value_min'] > 4294967295 || $reportData['deal_value_min'] < 0) {\n throw new InvalidArgumentException('Min deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_deal_value'])) {\n $reportData['deal_value_max'] = (int) $data['max_deal_value'];\n\n if ($reportData['deal_value_max'] > 4294967295 || $reportData['deal_value_max'] < 0) {\n throw new InvalidArgumentException('Max deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['min_deal_value'], $data['max_deal_value'])\n && $data['min_deal_value'] > $data['max_deal_value']\n ) {\n throw new InvalidArgumentException('Min deal value cannot be greater than max deal value');\n }\n\n return $reportData;\n }\n\n private function validateDateRange(array $data, array $reportData, string $frequency): array\n {\n // Set date range only for one_off frequency\n if ($frequency === 'one_off') {\n if (isset($data['start_date_period'])) {\n $reportData['from'] = $this->parseDate($data['start_date_period']);\n }\n\n if (isset($data['end_date_period'])) {\n $reportData['to'] = $this->parseDate($data['end_date_period']);\n }\n\n if (empty($reportData['from']) || empty($reportData['to'])) {\n throw new InvalidArgumentException(\n 'Start date and end date are required for one_off frequency'\n );\n }\n } else {\n $reportData['from'] = null;\n $reportData['to'] = null;\n }\n\n return $reportData;\n }\n\n private function validateCallDurations(array $data, array $reportData): array\n {\n // Convert call durations from minutes to seconds\n if (isset($data['min_call_duration'])) {\n $reportData['call_duration_min'] = (int) $data['min_call_duration'] * 60;\n\n if ($reportData['call_duration_min'] > 4294967295 || $reportData['call_duration_min'] < 0) {\n throw new InvalidArgumentException('Min call duration should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_call_duration'])) {\n $reportData['call_duration_max'] = (int) $data['max_call_duration'] * 60;\n\n if ($reportData['call_duration_max'] > 4294967295 || $reportData['call_duration_max'] < 0) {\n throw new InvalidArgumentException('Max call duration should be between 0 and 4294967295');\n }\n }\n\n return $reportData;\n }\n\n private function validateCallTypes(array $data, array $reportData): array\n {\n // Set call types\n $reportData['call_types'] = $data['call_type'] ?? [];\n if (empty($reportData['call_types'])) {\n $reportData['call_types'] = self::getCallTypes();\n }\n\n foreach ($reportData['call_types'] as $callType) {\n if (! in_array($callType, self::getCallTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Call type %s is invalid', $callType));\n }\n }\n\n return $reportData;\n }\n\n private function validateMediaTypes(array $data, array $reportData): array\n {\n // Set media types from input data\n $reportData['media_types'] = $data['media_types'] ?? [];\n\n if (empty($reportData['media_types'])) {\n throw new InvalidArgumentException('Media types are required');\n }\n\n foreach ($reportData['media_types'] as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n throw new InvalidArgumentException(sprintf('Media type %s is invalid', $mediaType));\n }\n }\n\n return $reportData;\n }\n\n private function validateDealStages(array $data, array $reportData, Team $team, string $reportType): array\n {\n // Validate and set deal stages\n if (isset($data['deal_stage_at_call'])) {\n $reportData['deal_at_call_stages'] =\n $this->validateAndGetDealStageIds($team, $data['deal_stage_at_call'], 'Deal stage at call');\n }\n\n if (isset($data['current_deal_stage'])) {\n $reportData['current_deal_stages'] =\n $this->validateAndGetDealStageIds($team, $data['current_deal_stage'], 'Current deal stage');\n }\n\n // Ensure current_deal_stage is not provided for loss_analysis report type\n if ($reportType === self::TYPE_LOSS_ANALYSIS && ! empty($data['current_deal_stage'])) {\n throw new InvalidArgumentException('Current deal stage is not applicable for Loss Analysis reports');\n }\n\n return $reportData;\n }\n\n // transform uuid to id\n private function validatePlaybookCategories(array $data, array $reportData, Team $team): array\n {\n $key = 'playbook_categories';\n\n if (isset($data[$key])) {\n $payloadIds = $data[$key];\n $ids = [];\n\n foreach ($payloadIds as $uuid) {\n $uuid = (string) $uuid;\n\n try {\n $playbookCategory = $this->playbookCategoryRepository->findByUuid($uuid);\n } catch (Throwable $throwable) {\n Log::error(__METHOD__ . ' ' . $throwable->getMessage());\n\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory) {\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory->hasPlaybook()) {\n throw new InvalidArgumentException(sprintf('Playbook category %s has no playbook', $uuid));\n }\n\n if ($playbookCategory->getPlaybook()->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Playbook category %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $ids[] = $playbookCategory->getId();\n }\n\n $reportData[$key] = $ids;\n }\n\n return $reportData;\n }\n\n private function validateReportStatus($status): bool\n {\n if (! in_array($status, [true, false], true)) {\n throw new InvalidArgumentException('Report status is invalid');\n }\n\n return $status;\n }\n\n private function validateReportType($type): string\n {\n if (! in_array($type, self::getTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Report type is invalid: %s', $type));\n }\n\n return $type;\n }\n\n private function validateFrequency($frequency): string\n {\n if (! in_array($frequency, self::getFrequencies(), true)) {\n throw new InvalidArgumentException('Frequency is invalid');\n }\n\n return $frequency;\n }\n\n private function validateAdditionalPromptInput(?string $additionalPromptInput): ?string\n {\n if ($additionalPromptInput && strlen($additionalPromptInput) > 5000) {\n throw new InvalidArgumentException('Additional Prompt Input should be less than 5000 characters');\n }\n\n return $additionalPromptInput;\n }\n\n private function validateCustomReportName(?string $customReportName): ?string\n {\n if ($customReportName === null || $customReportName === '') {\n return null;\n }\n\n if (strlen($customReportName) > 70) {\n throw new InvalidArgumentException('Custom report name should be less than 70 characters');\n }\n\n return $customReportName;\n }\n\n private function validateOrganization(?string $organizationUuid): Team\n {\n if (! $organizationUuid) {\n throw new InvalidArgumentException('Organization is required');\n }\n\n $team = $this->teamRepository->idOrUuid($organizationUuid);\n\n if (! $team) {\n throw new InvalidArgumentException('Organization not found');\n }\n\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n throw new InvalidArgumentException('Organization does not have the Automated Reports feature');\n }\n\n return $team;\n }\n\n private function validateAndGetGroupIds(Team $team, array $teamUuids): array\n {\n $groupIds = [];\n\n foreach ($teamUuids as $uuid) {\n $group = $this->groupRepository->findByUuid($uuid);\n\n if ($group === null || $group->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Group %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $groupIds[] = $group->getId();\n\n }\n\n return $groupIds;\n }\n\n private function validateAndGetDealStageIds(Team $team, array $stageUuids, string $propertyLabel): array\n {\n $stageIds = [];\n\n foreach ($stageUuids as $uuid) {\n $stage = $this->stageRepository->findByUuid($uuid);\n\n if ($stage === null || $stage->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Stage %s not found for team %s for %s', $uuid, $team->getUuid(), $propertyLabel)\n );\n }\n\n $stageIds[] = $stage->getId();\n }\n\n return $stageIds;\n }\n\n private function validateAndGetUserIds(array $userUuids, callable $teamCheck): array\n {\n if (empty($userUuids)) {\n return [];\n }\n\n $userIds = [];\n\n foreach ($userUuids as $uuid) {\n $user = $this->userRepository->findByUuid($uuid);\n\n if (! $user || ! $user->isStatusActive()) {\n throw new InvalidArgumentException(\n sprintf('User %s not found or is not active', $uuid)\n );\n }\n\n if (! $teamCheck($user)) {\n throw new InvalidArgumentException(\n sprintf('User %s does not belong to the allowed team(s)', $uuid)\n );\n }\n\n $userIds[] = $user->getId();\n }\n\n return $userIds;\n }\n\n private function validateAndGetUserIdsByTeam(Team $team, array $userUuids): array\n {\n return $this->validateAndGetUserIds($userUuids, fn ($user) => $user->getTeamId() === $team->getId());\n }\n\n private function validateAndGetJiminnyUserIds(array $userUuids): array\n {\n $allowedTeamIds = config('kiosk.teamIds', []);\n\n return $this->validateAndGetUserIds($userUuids, fn ($user) => in_array($user->getTeamId(), $allowedTeamIds, true));\n }\n\n private function parseDate(string $dateString): string\n {\n return date('Y-m-d H:i:s', strtotime($dateString));\n }\n\n private function generateReportResultViewUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.view', ['uuid' => $result->getUuid()]);\n }\n\n private function generateReportResultDownloadUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.download', ['uuid' => $result->getUuid()]);\n }\n\n private function getReportMediaRouteResource(AutomatedReportResult $result): string\n {\n if ($result->getMediaType() === self::MEDIA_TYPE_PDF) {\n return self::PDF_KEY;\n } elseif ($result->getMediaType() === self::MEDIA_TYPE_PODCAST) {\n return self::AUDIO_KEY;\n }\n\n throw new \\InvalidArgumentException('Unknown media type.');\n }\n\n public function getMediaPath(AutomatedReportResult $result): ?string\n {\n $url = match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => $result->getPdfUrl(),\n self::MEDIA_TYPE_PODCAST => $result->getPodcastAudioUrl(),\n default => null,\n };\n\n if ($url === null) {\n return null;\n }\n\n $path = parse_url(trim($url, '\"\\''), PHP_URL_PATH);\n\n return $path ?: null;\n }\n\n public function getFilenameSuffix(AutomatedReportResult $result): ?string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => 'Podcast',\n default => null,\n };\n }\n\n public function getMailSubjectSuffix(AutomatedReportResult $result): string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => 'report',\n self::MEDIA_TYPE_PODCAST => 'podcast',\n default => '',\n };\n }\n\n public function getMediaTypeMetadata(AutomatedReportResult $result): array\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => ['extension' => 'mp3', 'mime' => 'audio/mpeg'],\n self::MEDIA_TYPE_PDF => ['extension' => 'pdf', 'mime' => 'application/pdf'],\n default => ['extension' => null, 'mime' => null],\n };\n }\n\n public function deleteS3Files(AutomatedReportResult $result): void\n {\n $teamUuid = $result->getReport()->getTeam()->getUuid();\n $reportUuid = $result->getUuid();\n\n // delete all files for a report uuid no mather of pdf, podcast, or both\n // in case of both - the podcast files are linked to the pdf (parent) uuid\n // pdf and podcast date times should be close\n $path = sprintf('%s/%s/%s', $teamUuid, self::S3_DIR, $reportUuid);\n\n foreach (self::FILE_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted S3 file', [\n 'path' => $file,\n ]);\n }\n }\n\n foreach (self::FILE_PODCAST_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '_podcast.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted Podcast S3 file', [\n 'path' => $file,\n ]);\n }\n }\n }\n\n /**\n *\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): Collection\n {\n return $this->automatedReportsRepository->getTeamIdsWithReportsResults($teamId);\n }\n\n /**\n * Core delete logic for report results using a query\n *\n * @param Builder $query\n * @param array $logContext\n *\n * @return int\n */\n private function deleteReportResultsByQuery(Builder $query, array $logContext = []): int\n {\n $deletedCount = 0;\n\n if ($query->exists()) {\n Log::info(\n 'Run delete report results',\n array_merge(\n $logContext,\n [\n 'service' => 'AutomatedReportsService',\n ]\n )\n );\n\n $query->chunkById(50, function ($results) use (&$deletedCount, $logContext) {\n foreach ($results as $result) {\n $this->deleteReportResult($result);\n $deletedCount++;\n\n Log::info(\n 'Deleted a report result',\n array_merge(\n $logContext,\n [\n 'result_id' => $result->getId(),\n 'report_id' => $result->getReportId(),\n ]\n )\n );\n }\n });\n }\n\n return $deletedCount;\n }\n\n /**\n * Delete report results for a team by retention period\n *\n * @param Team $team\n * @param CarbonImmutable $retentionDate\n *\n * @return int Number of deleted report results\n */\n public function deleteReportsResultsInRetentionPeriod(Team $team, CarbonImmutable $retentionDate): int\n {\n $reportIds = $this->automatedReportsRepository->getReportIdsByTeam($team);\n\n if ($reportIds->isEmpty()) {\n return 0;\n }\n\n $query = $this->automatedReportsRepository\n ->getReportResultsQueryForRetention($team, $retentionDate);\n\n return $this->deleteReportResultsByQuery($query, [\n 'team_id' => $team->getId(),\n 'retention_date' => $retentionDate->toDateTimeString(),\n ]);\n }\n\n /**\n * Delete ALL report results for a specific automated report\n *\n * @param string $uuid\n *\n * @return int\n */\n public function deleteReportResults(string $uuid): int\n {\n $report = $this->getReport($uuid);\n\n $query = $this->automatedReportsRepository->getResultsByReportQuery($report);\n\n return $this->deleteReportResultsByQuery($query, [\n 'report_uuid' => $uuid,\n 'report_id' => $report->getId(),\n ]);\n }\n\n public function deleteReportResult(AutomatedReportResult $result): void\n {\n $this->deleteS3Files($result);\n\n $result->delete();\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getTeamReports(Team $team): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getReportsByTeam($team);\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getReportResults(AutomatedReport $report): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getResultsByReport($report);\n }\n\n public function deleteAllReportResults(AutomatedReport $report): void\n {\n $results = $this->getReportResults($report);\n\n /** @var AutomatedReportResult $result */\n foreach ($results as $result) {\n Log::info('Deleting result', [\n 'report' => $report->getId(),\n 'result' => $result->getId(),\n ]);\n\n $this->deleteReportResult($result);\n }\n }\n\n public function deleteAllData(Team $team): void\n {\n Log::info('Deleting automated report and results for team', [\n 'team' => $team->getId(),\n ]);\n\n $reports = $this->getTeamReports($team);\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n Log::info('Deleting report', [\n 'team' => $team->getId(),\n 'report' => $report->getId(),\n ]);\n\n $this->deleteAllReportResults($report);\n\n $report->delete();\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.42785904,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.43650267,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.4474734,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.45611703,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.46476063,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.47573137,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.4867021,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"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.51329786,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.5242686,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.70611703,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"21","depth":4,"bounds":{"left":0.66921544,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.68085104,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"18","depth":4,"bounds":{"left":0.69015956,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.7017952,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.7117686,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.7287234,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"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 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 * 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 `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\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;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","depth":4,"on_screen":true,"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 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 * 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 `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\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;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Socket fail to connect to host:address=(host=localhost)(port=3306)(type=primary). Connection refused","depth":3,"bounds":{"left":0.42652926,"top":0.9577015,"width":0.29321808,"height":0.013567438},"on_screen":true,"value":"Socket fail to connect to host:address=(host=localhost)(port=3306)(type=primary). Connection refused","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1751808696220340334
|
1126710648141684156
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskAnythingPromptServiceTest
Run 'AskAnythingPromptServiceTest'
Debug 'AskAnythingPromptServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
100
3
34
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Kiosk\AutomatedReports;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Carbon\Exceptions\InvalidFormatException;
use DateTime;
use DateTimeInterface;
use DateTimeZone;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\ActivitySearch\FilterDefinition\InputTypeEnum;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Partner;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Throwable;
class AutomatedReportsService
{
public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';
public const string TYPE_ASK_JIMINNY = 'ask_jiminny';
/**
* Standard report types (used by kiosk for existing automated reports).
*/
// @TODO this will add filter, however if we need to control feature by FF we need conditional logic
public const array TYPES = [
['id' => 'exec_summary', 'name' => 'Exec Summary'],
['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],
['id' => 'product_feedback', 'name' => 'Product Feedback'],
['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],
// ['id' => 'questions', 'name' => 'Questions'],
// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],
];
public const array ALL_TYPES = [
...self::TYPES,
['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],
];
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';
/**
* Frequencies for standard (non-Ask Jiminny) reports.
*/
public const array FREQUENCIES = [
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
/**
* Frequencies for Ask Jiminny reports.
*/
public const array ASK_JIMINNY_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
];
public const string MEDIA_TYPE_PDF = 'pdf';
public const string MEDIA_TYPE_PODCAST = 'podcast';
public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];
public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];
public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];
public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];
public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];
public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];
public const int SENT_REPORT_AT_HOURS = 5;
public const string PDF_KEY = 'pdf';
public const string AUDIO_KEY = 'audio';
private const array ALL_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
private const string S3_DIR = 'reports';
private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];
private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly GroupRepository $groupRepository,
private readonly UserRepository $userRepository,
private readonly StageRepository $stageRepository,
private readonly DealStagesService $dealStagesService,
private readonly RecipientsService $recipientsService,
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly Webhook $webhookService,
private readonly BusDispatcher $dispatcher,
private readonly ActivityTypeService $activityTypeService,
private readonly PlaybookCategoryRepository $playbookCategoryRepository,
private readonly AskAnythingPromptService $askAnythingPromptService,
private readonly SearchRepository $activitySearchRepository,
private readonly AskAnythingRepository $askAnythingRepository,
) {
}
public static function getTypes(): array
{
$types = self::TYPES;
return array_map(static function ($type) {
return $type['id'];
}, $types);
}
public static function getCallTypes(): array
{
return array_map(static function ($callType) {
return $callType['id'];
}, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);
}
public static function getFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::FREQUENCIES);
}
// front-facing structure
public function getReportEnabledFieldData(bool $value = false): array
{
return [
'id' => 'report_enabled',
'label' => '',
'inputType' => InputTypeEnum::TOGGLE,
'value' => $value,
];
}
// Organizations = Teams
public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array
{
$options = $this->getTeams(partner: $partner);
if ($shortVersion) {
return [
'id' => 'organization',
'label' => 'Organization',
'options' => $options,
];
}
return [
'id' => 'organization',
'label' => 'Organization',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $options,
'value' => $value,
'dependencies' => [
'teams',
'deal_stage_at_call',
'current_deal_stage',
'recipients',
ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,
],
'dependsOn' => [],
];
}
// Teams = Groups
public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array
{
if ($shortVersion) {
return [
'id' => 'teams',
'label' => 'Team',
'options' => $options,
];
}
return [
'id' => 'teams',
'label' => 'Team',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => false,
'placeholder' => 'Select',
'options' => $options,
'value' => $value, // value should be an array of objects {id, name}
'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],
'dependsOn' => [],
];
}
public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array
{
$types = [];
if ($team instanceof Team) {
if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
$types = self::TYPES;
}
if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {
$types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];
}
} else {
$types = self::TYPES;
}
if ($shortVersion) {
return [
'id' => 'report_type',
'label' => 'Report Type',
'options' => $types,
];
}
return [
'id' => 'report_type',
'label' => 'Report Type',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $types,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getFrequencyFieldData(?string $value = null): array
{
return [
'id' => 'frequency',
'label' => 'Frequency',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => self::FREQUENCIES,
'value' => $value,
'dependencies' => ['period'],
'dependsOn' => [],
];
}
public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array
{
return [
'id' => 'period',
'label' => 'Select one-off period',
'inputType' => InputTypeEnum::DATE_RANGE,
'required' => true,
'placeholder' => 'Select',
'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],
'queryParams' => [
'startDate' => 'start_date_period',
'endDate' => 'end_date_period',
],
'dependencies' => [],
'dependsOn' => ['frequency'],
];
}
public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array
{
return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);
}
public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);
}
public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);
}
public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'deal_value',
'label' => 'Deal Value',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_deal_value',
'max' => 'max_deal_value',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array
{
$value = [];
$conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;
$dialerOn && $value[] = self::CALL_TYPE_DIALER;
return [
'id' => 'call_type',
'label' => 'Call Type',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => [
self::CALL_TYPE_CONFERENCE,
self::CALL_TYPE_DIALER,
],
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getMediaTypeFieldData(?AutomatedReport $report = null): array
{
$value = [];
if ($report) {
$value = $this->transformMediaTypes($report);
}
return [
'id' => 'media_types',
'label' => 'Export as',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => self::MEDIA_TYPE_OBJECTS,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'call_duration',
'label' => 'Call Duration',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_call_duration',
'max' => 'max_call_duration',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getRecipientsFieldData(?Team $team = null, array $value = []): array
{
return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);
}
public function getJiminnyRecipientsFieldData(array $value = []): array
{
return $this->recipientsService->getJiminnyRecipientsFieldData($value);
}
public function getAdditionalPromptInputFieldData(?string $value = null): array
{
return [
'id' => 'additional_prompt_input',
'label' => 'Special requirements',
'inputType' => InputTypeEnum::TEXTAREA,
'required' => false,
'placeholder' => 'What should be the focus of the report?',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCustomReportNameFieldData(?string $value = null): array
{
return [
'id' => 'custom_name',
'label' => 'Custom report name',
'inputType' => InputTypeEnum::TEXT,
'required' => false,
'placeholder' => 'Enter custom name',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
// data providers
public function getTeams(?Partner $partner = null): array
{
$teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);
$teamData = [];
foreach ($teams as $team) {
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
continue;
}
$teamData[] = $this->transformTeam($team);
}
return $teamData;
}
public function getTeamGroups(string $teamUuid): array
{
$data = [];
$team = $this->getTeam($teamUuid);
if ($team !== null) {
$groups = $team->groups()->get();
foreach ($groups as $group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
];
}
}
return $data;
}
public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array
{
$data = [];
$teams = $this->getTeams(partner: $partner);
foreach ($teams as $team) {
if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {
continue;
}
$data[] = [
'label' => $team['name'],
'groups' => $this->getTeamGroups($team['id']),
];
}
return $data;
}
public function getTeam(string $teamUuid): ?Team
{
return $this->teamRepository->idOrUuid($teamUuid);
}
public function getTeamById(int $teamId): ?Team
{
return $this->teamRepository->find($teamId);
}
public function getGroupsUuids(AutomatedReport $report): array
{
$uuids = [];
$reportGroups = $report->getGroups();
foreach ($reportGroups as $groupId) {
if ($group = $this->groupRepository->find($groupId)) {
$uuids[] = $group->getUuid();
}
}
return $uuids;
}
public function getPlaybookCategoriesUuids(AutomatedReport $report): array
{
$uuids = [];
$playbookCategories = $report->getPlaybookCategories();
foreach ($playbookCategories as $id) {
if ($category = $this->playbookCategoryRepository->find($id)) {
$uuids[] = $category->getUuid();
}
}
return $uuids;
}
public function getDealAtCallStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getDealAtCallStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getCurrentDealStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getCurrentDealStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getRecipients());
}
public function getJiminnyUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function extractUserUuids(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => $user->getUuid())
->values()
->all();
}
// get mail data
public function getRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getRecipients());
}
/**
* @return array<UserContract>
*/
public function getRecipientUserObjects(AutomatedReport $report): array
{
$userIds = $report->getRecipients()['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->values()
->all();
}
private function getJiminnyRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function buildRecipientUsers(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => [
'email' => $user->getEmailAddress(),
'name' => $user->getName(),
'timezone' => $user->getTimezone()->getName(),
])
->values()
->all();
}
public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array
{
if ($report->isAskJiminnyReport()) {
$recipients = $this->resolveAskJiminnyRecipients($report);
} else {
$recipients = $this->getRecipientUsers($report);
if ($includeJiminny) {
$recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));
}
}
$emails = [];
return array_values(array_filter(
$recipients,
static function ($recipient) use (&$emails) {
if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {
return false;
}
$emails[] = $recipient['email'];
return true;
}
));
}
private function resolveAskJiminnyRecipients(AutomatedReport $report): array
{
$recipients = [];
$creator = $report->getCreator();
if ($creator !== null) {
$recipients[] = [
'email' => $creator->getEmailAddress(),
'name' => $creator->getName(),
'timezone' => $creator->getTimezone()->getName(),
];
}
return array_merge(
$recipients,
$this->buildRecipientUsers($report->getRecipients()),
$this->getGroupRecipientUsers($report),
);
}
private function getGroupRecipientUsers(AutomatedReport $report): array
{
$users = [];
foreach ($report->getGroups() as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group === null) {
continue;
}
foreach ($group->getMembers() as $member) {
$users[] = [
'email' => $member->getEmailAddress(),
'name' => $member->getName(),
'timezone' => $member->getTimezone()->getName(),
];
}
}
return $users;
}
public function getReportTypeName(AutomatedReportResult $report): string
{
$type = $report->getReport()->getType();
$getType = $this->transformReportType($type);
return $getType['name'];
}
public function getReportPeriodName(AutomatedReportResult $report): string
{
$from = $report->getFromDate();
$to = $report->getToDate();
$frequency = $report->getReport()->getFrequency();
if ($from === null || $to === null) {
if (! $report->getReport()->isAskJiminnyReport()) {
$invalidPeriod = $from === null ? 'from' : 'to';
throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);
}
$timezone = $report->getReport()->getCreator()?->getTimezone();
$period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);
$from = $period['fromDate'];
$to = $period['toDate'];
}
return $this->formatReportPeriodName($frequency, $from, $to);
}
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 getReportTeamsName(AutomatedReportResult $report): string
{
$groups = $report->getGroups();
if (empty($groups)) {
return 'All';
}
// Get group names from repository
$groupNames = [];
foreach ($groups as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group) {
$groupNames[] = $group->getName();
}
}
if (count($groupNames) === 1) {
// Single team format
$teamsName = $groupNames[0];
} else {
// Multiple teams format
$teamsName = implode(', ', $groupNames);
}
return $teamsName;
}
public function getReportFileName(AutomatedReportResult $report): string
{
$customName = $report->getReport()->getCustomName();
$periodName = $this->getReportPeriodName($report);
$filenameSuffix = $this->getFilenameSuffix($report);
if ($customName) {
if ($filenameSuffix) {
$customName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$customName} - {$periodName}");
}
$baseName = $this->getReportTypeName($report);
if ($filenameSuffix) {
$baseName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}");
}
public function getReportFileNameWithExtension(AutomatedReportResult $result): string
{
$extension = $this->getMediaTypeMetadata($result)['extension'];
return $this->getReportFileName($result) . '.' . $extension;
}
public function sanitizeFileName(string $fileName): string
{
return str_replace(['/', '\\'], '-', $fileName);
}
public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool
{
$recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);
if (in_array($user->getId(), $recipientIds, true)) {
return true;
}
if ($report->isAskJiminnyReport()) {
$groupId = $user->getGroupId();
if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {
return true;
}
}
return false;
}
public function transformReportResults(Collection $automatedReportResults): array
{
$data = [];
foreach ($automatedReportResults as $automatedReportResult) {
/** @var AutomatedReportResult $automatedReportResult */
$report = $automatedReportResult->getReport();
$createdBy = $report->getCreator();
$creator = [
'id' => $createdBy?->getUuid(),
'name' => $createdBy?->getName(),
'email' => $createdBy?->getEmailAddress(),
'photoUrl' => $createdBy?->getPhotoUrl(),
];
$data[] = [
'id' => $automatedReportResult->getUuid(),
'name' => $automatedReportResult->getName(),
'frequency' => $this->transformFrequency($report->getFrequency()),
'recipients' => $this->buildRecipients($report),
'report_type' => $this->transformReportType($report->getType()),
'media_type' => $automatedReportResult->getMediaType(),
'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),
'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),
'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),
'creator' => $creator,
];
}
return $data;
}
private function buildRecipients(AutomatedReport $report): array
{
$creatorUuid = $report->getCreator()?->getUuid();
$recipients = array_values(array_filter(
$this->transformRecipients($report->getRecipients()),
static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,
));
if (! $report->isAskJiminnyReport()) {
return $recipients;
}
return [
...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),
...$recipients,
];
}
public function hasCallTypeConference(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);
}
public function hasCallTypeDialer(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);
}
// transformers
private function transformTeam(Team $team): array
{
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
return [];
}
return [
'id' => $team->getUuid(),
'name' => $team->getName(),
];
}
private function transformReportFullView(AutomatedReport $report): array
{
$base = $this->transformReportBase($report);
return $report->getType() === self::TYPE_ASK_JIMINNY
? $base + $this->transformAskJiminnyFields($report)
: $base + $this->transformStandardReportFields($report);
}
private function transformReportBase(AutomatedReport $report): array
{
return [
'id' => $report->getUuid(),
'organization' => $this->transformOrganization(team: $report->getTeam()),
'report_type' => $this->transformReportType($report->getType()),
'frequency' => $this->transformFrequency($report->getFrequency()),
];
}
private function transformStandardReportFields(AutomatedReport $report): array
{
$team = $report->getTeam();
return [
'report_enabled' => $report->getStatus(),
'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),
'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),
'deal_value_min' => $report->getDealValueMin(),
'deal_value_max' => $report->getDealValueMax(),
'call_types' => $this->transformCallType($report->getCallTypes()),
'media_types' => $this->transformMediaTypes($report),
'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),
'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),
'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),
'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),
'recipients' => $this->transformRecipients($report->getRecipients()),
'created_by' => $this->transformCreator($report->getCreator()),
'additional_prompt_input' => $report->getAdditionalPromptInput(),
'custom_name' => $report->getCustomName(),
'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),
'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),
'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),
];
}
private function transformAskJiminnyFields(AutomatedReport $report): array
{
$team = $report->getTeam();
$creatorId = $report->getAttribute('created_by');
$explicitUserIds = array_values(array_filter(
$report->getRecipients()['users'] ?? [],
static fn ($id) => $id !== $creatorId
));
return [
'report_name' => $report->getCustomName(),
'enabled' => $report->getStatus(),
'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),
'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),
'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),
'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),
];
}
private function transformOrganization(?Team $team): array
{
return [
'id' => $team?->getUuid(),
'name' => $team?->getName(),
];
}
private function transformReportType(string $type): array
{
foreach (self::ALL_TYPES as $typeItem) {
if ($typeItem['id'] === $type) {
return $typeItem;
}
}
return [];
}
private function transformCallType(array $types): array
{
$result = [];
$callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];
foreach ($types as $type) {
foreach ($callTypes as $callTypeItem) {
if ($callTypeItem['id'] === $type) {
$result[] = $callTypeItem;
break;
}
}
}
return $result;
}
private function transformMediaTypes(AutomatedReport $report): array
{
$values = [];
foreach ($report->getMediaTypes() as $mediaType) {
if (! in_array($mediaType, self::MEDIA_TYPES, true)) {
continue;
}
$values[] = match ($mediaType) {
self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,
self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,
};
}
return $values;
}
private function transformFrequency(string $frequency): array
{
foreach (self::ALL_FREQUENCIES as $frequencyItem) {
if ($frequencyItem['id'] === $frequency) {
return $frequencyItem;
}
}
return [];
}
public function transformDurationToMinutes(?int $duration): ?int
{
if (! $duration) {
return null;
}
return (int) ($duration / 60);
}
private function transformGroups(?Team $team, array $groupsIds): array
{
if (empty($groupsIds) || ! $team) {
return [];
}
$data = [];
foreach ($groupsIds as $groupId) {
$group = $team->groups()->where('id', $groupId)->first();
if ($group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
'photoUrl' => $group->getPhotoUrl(),
];
}
}
return $data;
}
private function transformStages(?Team $team, array $stagesIds): array
{
if (empty($stagesIds) || ! $team) {
return [];
}
$data = [];
foreach ($stagesIds as $stageId) {
$stage = $team->stages()->where('id', $stageId)->first();
if ($stage) {
$data[] = [
'id' => $stage->getUuid(),
'name' => $stage->getName(),
];
}
}
return $data;
}
private function transformRecipients(array $recipients): array
{
$users = [];
foreach ($recipients['users'] ?? [] as $userId) {
$users[] = $this->transformUser($userId);
}
return $users;
}
private function transformCreator(?User $user): ?array
{
if ($user === null) {
return null;
}
return $this->transformUser($user->getId());
}
private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array
{
if ($prompt === null) {
return null;
}
return [
'id' => $prompt->getUuid(),
'name' => $prompt->getTitle(),
];
}
private function transformSafeSearch(?Search $search): ?array
{
if ($search === null) {
return null;
}
return [
'id' => $search->getUuid(),
'name' => $search->getName(),
];
}
private function transformUser(int $userId): array
{
/* @var ?User $user */
$user = $this->userRepository->find($userId);
return [
'id' => $user?->getUuid(),
'name' => $user?->getName(),
'email' => $user?->getEmailAddress(),
'photoUrl' => $user?->getPhotoUrl(),
];
}
public function create(array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$validatedData['created_by'] = auth()->id();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
public function update(string $uuid, array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$report = $this->automatedReportsRepository->findByUuid($uuid);
if (! $report) {
throw new InvalidArgumentException('Report not found');
}
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
/**
* Create an Ask Jiminny report.
*/
public function createAskJiminnyReport(array $data, User $creator): array
{
$validatedData = $this->validateAskJiminnyReportData($data, $creator);
$validatedData['created_by'] = $creator->getId();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
return $this->transformReportFullView($automatedReport);
}
/**
* Update an Ask Jiminny report.
*/
public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array
{
if (! $report->isAskJiminnyReport()) {
throw new InvalidArgumentException('Report is not an Ask Jiminny report');
}
$validatedData = $this->validateAskJiminnyReportData($data, $user);
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
return $this->transformReportFullView($automatedReport);
}
public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array
{
if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {
throw new InvalidArgumentException(
'This report is missing a saved search or prompt. ' .
'Edit the report to complete the setup before enabling it.'
);
}
$this->automatedReportsRepository->update($report, ['status' => $status]);
return $this->transformReportFullView($report->fresh());
}
/**
* Validate and transform data for Ask Jiminny reports.
*/
private function validateAskJiminnyReportData(array $data, User $user): array
{
// Validate name
$name = trim($data['report_name'] ?? '');
if (empty($name)) {
throw new InvalidArgumentException('Report name is required');
}
if (mb_strlen($name) > 50) {
throw new InvalidArgumentException('Report name must be 50 characters or less');
}
// Validate frequency (only daily, weekly, monthly for Ask Jiminny)
$frequency = $data['frequency'] ?? null;
$askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];
if (! in_array($frequency, $askJiminnyFrequencies, true)) {
throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');
}
// Validate expiration date
$expiresAt = $data['expires_on'] ?? null;
if (empty($expiresAt)) {
throw new InvalidArgumentException('Expiration date is required');
}
try {
$expiresAtDate = Carbon::parse($expiresAt);
} catch (InvalidFormatException $e) {
throw new InvalidArgumentException('Expiration date format is invalid');
}
$maxExpiration = Carbon::now()->addYear()->endOfDay();
if ($expiresAtDate->gt($maxExpiration)) {
throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');
}
if ($expiresAtDate->isPast()) {
throw new InvalidArgumentException('Expiration date cannot be in the past');
}
// Validate saved search
$activitySearchId = $data['saved_search'] ?? null;
if (empty($activitySearchId)) {
throw new InvalidArgumentException('Saved search is required');
}
$savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);
if (! $savedSearch) {
throw new InvalidArgumentException('Saved search not found or does not belong to you');
}
// Validate saved prompt
$askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;
if (empty($askAnythingPromptId)) {
throw new InvalidArgumentException('Ask Jiminny prompt is required');
}
$prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);
if (! $prompt) {
throw new InvalidArgumentException('Ask Jiminny prompt not found');
}
// Validate status
$status = $data['enabled'] ?? false;
$recipientUserIds = [$user->getId()];
if (! empty($data['share_users'])) {
$sharedUserIds = $this->validateAndGetUserIdsByTeam(
$user->team,
(array) $data['share_users']
);
$recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);
}
$sharedGroupIds = [];
if (! empty($data['share_teams'])) {
$sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);
}
$recipientUserIds = array_values(array_unique($recipientUserIds));
return [
'team_id' => $user->getTeamId(),
'type' => self::TYPE_ASK_JIMINNY,
'status' => (bool) $status,
'frequency' => $frequency,
'custom_name' => $name,
'activity_search_id' => $savedSearch->getId(),
'ask_anything_prompt_id' => $prompt->getId(),
'expires_at' => $expiresAtDate->toDateString(),
'media_types' => [self::MEDIA_TYPE_PDF],
'call_types' => [],
'recipients' => ['users' => $recipientUserIds],
'groups' => $sharedGroupIds,
];
}
public static function getAskJiminnyFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::ASK_JIMINNY_FREQUENCIES);
}
public function getAskJiminnyReportFilters(User $user): array
{
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
return [
[
'id' => 'prompt',
'label' => 'Prompt',
'options' => $prompts,
],
[
'id' => 'saved_search',
'label' => 'Saved Search',
'options' => $savedSearches,
],
];
}
public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array
{
$team = $user->getTeam();
$userTimezone = $user->getTimezone();
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
$teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [
'id' => $group->getUuid(),
'name' => $group->getName(),
])->values()->all();
$shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];
$sharedTeamsValue = [];
$sharedUsersValue = [];
if ($report) {
$sharedTeamsValue = $this->transformGroups($team, $report->getGroups());
$recipientUserIds = $report->getRecipients()['users'] ?? [];
$creatorId = $report->getAttribute('created_by');
$sharedUserIds = array_values(array_filter(
$recipientUserIds,
static fn ($id) => $id !== $creatorId
));
$sharedUsersValue = collect($sharedUserIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (User $u) => [
'id' => $u->getUuid(),
'name' => $u->getName(),
])
->values()
->all();
}
return [
'fields' => [
[
'id' => 'enabled',
'inputType' => InputTypeEnum::TOGGLE,
'label' => '',
'value' => $report?->getStatus() ?? false,
],
[
'id' => 'report_name',
'inputType' => InputTypeEnum::TEXT,
'label' => 'Name',
'placeholder' => 'Enter name',
'required' => true,
'validation' => ['maxLength' => 50],
'value' => $report?->getCustomName() ?? '',
...
|
57993
|
NULL
|
NULL
|
NULL
|
|
58018
|
2042
|
1
|
2026-05-19T11:26:41.664206+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190001664_m1.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8043719072324535154
|
-8628527368849355612
|
idle
|
hybrid
|
NULL
|
Project: faVsco.js, menu
SlackFileEditViewGoHistor Project: faVsco.js, menu
SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV (docker)₴82APPAPP (-zslFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20676-delete-report-related-obHomeDMsActivityFilesLater..•More>0.(ahlSupport Daily - in 34 m100% C7Tue 19 May 14:26:41ED→Describe what you are looking forJiminny... vscnicret# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Nikolay Yankov6 0• Messages+. Direct messages8. Nikolay Yankov% Galya DimitrovaGo Vasil Vasilev 00. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomova Todor Stamatov *Mario Georgievao Nikolay Ivanovdo James GrahamStoyan TanevLukas Kovalik y…..l:: AppsJira CloudToastAdd canvas@ Files/api/v2/u3 new messagests?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcalliзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message Serror = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNewNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Message Nikolay Yankov...
|
57994
|
NULL
|
NULL
|
NULL
|
|
58019
|
2042
|
2
|
2026-05-19T11:27:13.581188+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190033581_m1.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskAnythingPromptServiceTest
Run 'AskAnythingPromptServiceTest'
Debug 'AskAnythingPromptServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
100
3
34
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Kiosk\AutomatedReports;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Carbon\Exceptions\InvalidFormatException;
use DateTime;
use DateTimeInterface;
use DateTimeZone;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\ActivitySearch\FilterDefinition\InputTypeEnum;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Partner;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Throwable;
class AutomatedReportsService
{
public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';
public const string TYPE_ASK_JIMINNY = 'ask_jiminny';
/**
* Standard report types (used by kiosk for existing automated reports).
*/
// @TODO this will add filter, however if we need to control feature by FF we need conditional logic
public const array TYPES = [
['id' => 'exec_summary', 'name' => 'Exec Summary'],
['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],
['id' => 'product_feedback', 'name' => 'Product Feedback'],
['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],
// ['id' => 'questions', 'name' => 'Questions'],
// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],
];
public const array ALL_TYPES = [
...self::TYPES,
['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],
];
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';
/**
* Frequencies for standard (non-Ask Jiminny) reports.
*/
public const array FREQUENCIES = [
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
/**
* Frequencies for Ask Jiminny reports.
*/
public const array ASK_JIMINNY_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
];
public const string MEDIA_TYPE_PDF = 'pdf';
public const string MEDIA_TYPE_PODCAST = 'podcast';
public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];
public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];
public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];
public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];
public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];
public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];
public const int SENT_REPORT_AT_HOURS = 5;
public const string PDF_KEY = 'pdf';
public const string AUDIO_KEY = 'audio';
private const array ALL_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
private const string S3_DIR = 'reports';
private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];
private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly GroupRepository $groupRepository,
private readonly UserRepository $userRepository,
private readonly StageRepository $stageRepository,
private readonly DealStagesService $dealStagesService,
private readonly RecipientsService $recipientsService,
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly Webhook $webhookService,
private readonly BusDispatcher $dispatcher,
private readonly ActivityTypeService $activityTypeService,
private readonly PlaybookCategoryRepository $playbookCategoryRepository,
private readonly AskAnythingPromptService $askAnythingPromptService,
private readonly SearchRepository $activitySearchRepository,
private readonly AskAnythingRepository $askAnythingRepository,
) {
}
public static function getTypes(): array
{
$types = self::TYPES;
return array_map(static function ($type) {
return $type['id'];
}, $types);
}
public static function getCallTypes(): array
{
return array_map(static function ($callType) {
return $callType['id'];
}, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);
}
public static function getFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::FREQUENCIES);
}
// front-facing structure
public function getReportEnabledFieldData(bool $value = false): array
{
return [
'id' => 'report_enabled',
'label' => '',
'inputType' => InputTypeEnum::TOGGLE,
'value' => $value,
];
}
// Organizations = Teams
public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array
{
$options = $this->getTeams(partner: $partner);
if ($shortVersion) {
return [
'id' => 'organization',
'label' => 'Organization',
'options' => $options,
];
}
return [
'id' => 'organization',
'label' => 'Organization',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $options,
'value' => $value,
'dependencies' => [
'teams',
'deal_stage_at_call',
'current_deal_stage',
'recipients',
ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,
],
'dependsOn' => [],
];
}
// Teams = Groups
public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array
{
if ($shortVersion) {
return [
'id' => 'teams',
'label' => 'Team',
'options' => $options,
];
}
return [
'id' => 'teams',
'label' => 'Team',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => false,
'placeholder' => 'Select',
'options' => $options,
'value' => $value, // value should be an array of objects {id, name}
'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],
'dependsOn' => [],
];
}
public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array
{
$types = [];
if ($team instanceof Team) {
if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
$types = self::TYPES;
}
if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {
$types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];
}
} else {
$types = self::TYPES;
}
if ($shortVersion) {
return [
'id' => 'report_type',
'label' => 'Report Type',
'options' => $types,
];
}
return [
'id' => 'report_type',
'label' => 'Report Type',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $types,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getFrequencyFieldData(?string $value = null): array
{
return [
'id' => 'frequency',
'label' => 'Frequency',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => self::FREQUENCIES,
'value' => $value,
'dependencies' => ['period'],
'dependsOn' => [],
];
}
public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array
{
return [
'id' => 'period',
'label' => 'Select one-off period',
'inputType' => InputTypeEnum::DATE_RANGE,
'required' => true,
'placeholder' => 'Select',
'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],
'queryParams' => [
'startDate' => 'start_date_period',
'endDate' => 'end_date_period',
],
'dependencies' => [],
'dependsOn' => ['frequency'],
];
}
public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array
{
return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);
}
public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);
}
public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);
}
public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'deal_value',
'label' => 'Deal Value',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_deal_value',
'max' => 'max_deal_value',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array
{
$value = [];
$conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;
$dialerOn && $value[] = self::CALL_TYPE_DIALER;
return [
'id' => 'call_type',
'label' => 'Call Type',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => [
self::CALL_TYPE_CONFERENCE,
self::CALL_TYPE_DIALER,
],
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getMediaTypeFieldData(?AutomatedReport $report = null): array
{
$value = [];
if ($report) {
$value = $this->transformMediaTypes($report);
}
return [
'id' => 'media_types',
'label' => 'Export as',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => self::MEDIA_TYPE_OBJECTS,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'call_duration',
'label' => 'Call Duration',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_call_duration',
'max' => 'max_call_duration',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getRecipientsFieldData(?Team $team = null, array $value = []): array
{
return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);
}
public function getJiminnyRecipientsFieldData(array $value = []): array
{
return $this->recipientsService->getJiminnyRecipientsFieldData($value);
}
public function getAdditionalPromptInputFieldData(?string $value = null): array
{
return [
'id' => 'additional_prompt_input',
'label' => 'Special requirements',
'inputType' => InputTypeEnum::TEXTAREA,
'required' => false,
'placeholder' => 'What should be the focus of the report?',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCustomReportNameFieldData(?string $value = null): array
{
return [
'id' => 'custom_name',
'label' => 'Custom report name',
'inputType' => InputTypeEnum::TEXT,
'required' => false,
'placeholder' => 'Enter custom name',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
// data providers
public function getTeams(?Partner $partner = null): array
{
$teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);
$teamData = [];
foreach ($teams as $team) {
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
continue;
}
$teamData[] = $this->transformTeam($team);
}
return $teamData;
}
public function getTeamGroups(string $teamUuid): array
{
$data = [];
$team = $this->getTeam($teamUuid);
if ($team !== null) {
$groups = $team->groups()->get();
foreach ($groups as $group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
];
}
}
return $data;
}
public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array
{
$data = [];
$teams = $this->getTeams(partner: $partner);
foreach ($teams as $team) {
if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {
continue;
}
$data[] = [
'label' => $team['name'],
'groups' => $this->getTeamGroups($team['id']),
];
}
return $data;
}
public function getTeam(string $teamUuid): ?Team
{
return $this->teamRepository->idOrUuid($teamUuid);
}
public function getTeamById(int $teamId): ?Team
{
return $this->teamRepository->find($teamId);
}
public function getGroupsUuids(AutomatedReport $report): array
{
$uuids = [];
$reportGroups = $report->getGroups();
foreach ($reportGroups as $groupId) {
if ($group = $this->groupRepository->find($groupId)) {
$uuids[] = $group->getUuid();
}
}
return $uuids;
}
public function getPlaybookCategoriesUuids(AutomatedReport $report): array
{
$uuids = [];
$playbookCategories = $report->getPlaybookCategories();
foreach ($playbookCategories as $id) {
if ($category = $this->playbookCategoryRepository->find($id)) {
$uuids[] = $category->getUuid();
}
}
return $uuids;
}
public function getDealAtCallStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getDealAtCallStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getCurrentDealStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getCurrentDealStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getRecipients());
}
public function getJiminnyUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function extractUserUuids(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => $user->getUuid())
->values()
->all();
}
// get mail data
public function getRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getRecipients());
}
/**
* @return array<UserContract>
*/
public function getRecipientUserObjects(AutomatedReport $report): array
{
$userIds = $report->getRecipients()['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->values()
->all();
}
private function getJiminnyRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function buildRecipientUsers(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => [
'email' => $user->getEmailAddress(),
'name' => $user->getName(),
'timezone' => $user->getTimezone()->getName(),
])
->values()
->all();
}
public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array
{
if ($report->isAskJiminnyReport()) {
$recipients = $this->resolveAskJiminnyRecipients($report);
} else {
$recipients = $this->getRecipientUsers($report);
if ($includeJiminny) {
$recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));
}
}
$emails = [];
return array_values(array_filter(
$recipients,
static function ($recipient) use (&$emails) {
if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {
return false;
}
$emails[] = $recipient['email'];
return true;
}
));
}
private function resolveAskJiminnyRecipients(AutomatedReport $report): array
{
$recipients = [];
$creator = $report->getCreator();
if ($creator !== null) {
$recipients[] = [
'email' => $creator->getEmailAddress(),
'name' => $creator->getName(),
'timezone' => $creator->getTimezone()->getName(),
];
}
return array_merge(
$recipients,
$this->buildRecipientUsers($report->getRecipients()),
$this->getGroupRecipientUsers($report),
);
}
private function getGroupRecipientUsers(AutomatedReport $report): array
{
$users = [];
foreach ($report->getGroups() as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group === null) {
continue;
}
foreach ($group->getMembers() as $member) {
$users[] = [
'email' => $member->getEmailAddress(),
'name' => $member->getName(),
'timezone' => $member->getTimezone()->getName(),
];
}
}
return $users;
}
public function getReportTypeName(AutomatedReportResult $report): string
{
$type = $report->getReport()->getType();
$getType = $this->transformReportType($type);
return $getType['name'];
}
public function getReportPeriodName(AutomatedReportResult $report): string
{
$from = $report->getFromDate();
$to = $report->getToDate();
$frequency = $report->getReport()->getFrequency();
if ($from === null || $to === null) {
if (! $report->getReport()->isAskJiminnyReport()) {
$invalidPeriod = $from === null ? 'from' : 'to';
throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);
}
$timezone = $report->getReport()->getCreator()?->getTimezone();
$period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);
$from = $period['fromDate'];
$to = $period['toDate'];
}
return $this->formatReportPeriodName($frequency, $from, $to);
}
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 getReportTeamsName(AutomatedReportResult $report): string
{
$groups = $report->getGroups();
if (empty($groups)) {
return 'All';
}
// Get group names from repository
$groupNames = [];
foreach ($groups as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group) {
$groupNames[] = $group->getName();
}
}
if (count($groupNames) === 1) {
// Single team format
$teamsName = $groupNames[0];
} else {
// Multiple teams format
$teamsName = implode(', ', $groupNames);
}
return $teamsName;
}
public function getReportFileName(AutomatedReportResult $report): string
{
$customName = $report->getReport()->getCustomName();
$periodName = $this->getReportPeriodName($report);
$filenameSuffix = $this->getFilenameSuffix($report);
if ($customName) {
if ($filenameSuffix) {
$customName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$customName} - {$periodName}");
}
$baseName = $this->getReportTypeName($report);
if ($filenameSuffix) {
$baseName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}");
}
public function getReportFileNameWithExtension(AutomatedReportResult $result): string
{
$extension = $this->getMediaTypeMetadata($result)['extension'];
return $this->getReportFileName($result) . '.' . $extension;
}
public function sanitizeFileName(string $fileName): string
{
return str_replace(['/', '\\'], '-', $fileName);
}
public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool
{
$recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);
if (in_array($user->getId(), $recipientIds, true)) {
return true;
}
if ($report->isAskJiminnyReport()) {
$groupId = $user->getGroupId();
if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {
return true;
}
}
return false;
}
public function transformReportResults(Collection $automatedReportResults): array
{
$data = [];
foreach ($automatedReportResults as $automatedReportResult) {
/** @var AutomatedReportResult $automatedReportResult */
$report = $automatedReportResult->getReport();
$createdBy = $report->getCreator();
$creator = [
'id' => $createdBy?->getUuid(),
'name' => $createdBy?->getName(),
'email' => $createdBy?->getEmailAddress(),
'photoUrl' => $createdBy?->getPhotoUrl(),
];
$data[] = [
'id' => $automatedReportResult->getUuid(),
'name' => $automatedReportResult->getName(),
'frequency' => $this->transformFrequency($report->getFrequency()),
'recipients' => $this->buildRecipients($report),
'report_type' => $this->transformReportType($report->getType()),
'media_type' => $automatedReportResult->getMediaType(),
'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),
'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),
'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),
'creator' => $creator,
];
}
return $data;
}
private function buildRecipients(AutomatedReport $report): array
{
$creatorUuid = $report->getCreator()?->getUuid();
$recipients = array_values(array_filter(
$this->transformRecipients($report->getRecipients()),
static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,
));
if (! $report->isAskJiminnyReport()) {
return $recipients;
}
return [
...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),
...$recipients,
];
}
public function hasCallTypeConference(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);
}
public function hasCallTypeDialer(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);
}
// transformers
private function transformTeam(Team $team): array
{
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
return [];
}
return [
'id' => $team->getUuid(),
'name' => $team->getName(),
];
}
private function transformReportFullView(AutomatedReport $report): array
{
$base = $this->transformReportBase($report);
return $report->getType() === self::TYPE_ASK_JIMINNY
? $base + $this->transformAskJiminnyFields($report)
: $base + $this->transformStandardReportFields($report);
}
private function transformReportBase(AutomatedReport $report): array
{
return [
'id' => $report->getUuid(),
'organization' => $this->transformOrganization(team: $report->getTeam()),
'report_type' => $this->transformReportType($report->getType()),
'frequency' => $this->transformFrequency($report->getFrequency()),
];
}
private function transformStandardReportFields(AutomatedReport $report): array
{
$team = $report->getTeam();
return [
'report_enabled' => $report->getStatus(),
'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),
'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),
'deal_value_min' => $report->getDealValueMin(),
'deal_value_max' => $report->getDealValueMax(),
'call_types' => $this->transformCallType($report->getCallTypes()),
'media_types' => $this->transformMediaTypes($report),
'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),
'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),
'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),
'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),
'recipients' => $this->transformRecipients($report->getRecipients()),
'created_by' => $this->transformCreator($report->getCreator()),
'additional_prompt_input' => $report->getAdditionalPromptInput(),
'custom_name' => $report->getCustomName(),
'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),
'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),
'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),
];
}
private function transformAskJiminnyFields(AutomatedReport $report): array
{
$team = $report->getTeam();
$creatorId = $report->getAttribute('created_by');
$explicitUserIds = array_values(array_filter(
$report->getRecipients()['users'] ?? [],
static fn ($id) => $id !== $creatorId
));
return [
'report_name' => $report->getCustomName(),
'enabled' => $report->getStatus(),
'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),
'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),
'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),
'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),
];
}
private function transformOrganization(?Team $team): array
{
return [
'id' => $team?->getUuid(),
'name' => $team?->getName(),
];
}
private function transformReportType(string $type): array
{
foreach (self::ALL_TYPES as $typeItem) {
if ($typeItem['id'] === $type) {
return $typeItem;
}
}
return [];
}
private function transformCallType(array $types): array
{
$result = [];
$callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];
foreach ($types as $type) {
foreach ($callTypes as $callTypeItem) {
if ($callTypeItem['id'] === $type) {
$result[] = $callTypeItem;
break;
}
}
}
return $result;
}
private function transformMediaTypes(AutomatedReport $report): array
{
$values = [];
foreach ($report->getMediaTypes() as $mediaType) {
if (! in_array($mediaType, self::MEDIA_TYPES, true)) {
continue;
}
$values[] = match ($mediaType) {
self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,
self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,
};
}
return $values;
}
private function transformFrequency(string $frequency): array
{
foreach (self::ALL_FREQUENCIES as $frequencyItem) {
if ($frequencyItem['id'] === $frequency) {
return $frequencyItem;
}
}
return [];
}
public function transformDurationToMinutes(?int $duration): ?int
{
if (! $duration) {
return null;
}
return (int) ($duration / 60);
}
private function transformGroups(?Team $team, array $groupsIds): array
{
if (empty($groupsIds) || ! $team) {
return [];
}
$data = [];
foreach ($groupsIds as $groupId) {
$group = $team->groups()->where('id', $groupId)->first();
if ($group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
'photoUrl' => $group->getPhotoUrl(),
];
}
}
return $data;
}
private function transformStages(?Team $team, array $stagesIds): array
{
if (empty($stagesIds) || ! $team) {
return [];
}
$data = [];
foreach ($stagesIds as $stageId) {
$stage = $team->stages()->where('id', $stageId)->first();
if ($stage) {
$data[] = [
'id' => $stage->getUuid(),
'name' => $stage->getName(),
];
}
}
return $data;
}
private function transformRecipients(array $recipients): array
{
$users = [];
foreach ($recipients['users'] ?? [] as $userId) {
$users[] = $this->transformUser($userId);
}
return $users;
}
private function transformCreator(?User $user): ?array
{
if ($user === null) {
return null;
}
return $this->transformUser($user->getId());
}
private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array
{
if ($prompt === null) {
return null;
}
return [
'id' => $prompt->getUuid(),
'name' => $prompt->getTitle(),
];
}
private function transformSafeSearch(?Search $search): ?array
{
if ($search === null) {
return null;
}
return [
'id' => $search->getUuid(),
'name' => $search->getName(),
];
}
private function transformUser(int $userId): array
{
/* @var ?User $user */
$user = $this->userRepository->find($userId);
return [
'id' => $user?->getUuid(),
'name' => $user?->getName(),
'email' => $user?->getEmailAddress(),
'photoUrl' => $user?->getPhotoUrl(),
];
}
public function create(array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$validatedData['created_by'] = auth()->id();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
public function update(string $uuid, array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$report = $this->automatedReportsRepository->findByUuid($uuid);
if (! $report) {
throw new InvalidArgumentException('Report not found');
}
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
/**
* Create an Ask Jiminny report.
*/
public function createAskJiminnyReport(array $data, User $creator): array
{
$validatedData = $this->validateAskJiminnyReportData($data, $creator);
$validatedData['created_by'] = $creator->getId();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
return $this->transformReportFullView($automatedReport);
}
/**
* Update an Ask Jiminny report.
*/
public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array
{
if (! $report->isAskJiminnyReport()) {
throw new InvalidArgumentException('Report is not an Ask Jiminny report');
}
$validatedData = $this->validateAskJiminnyReportData($data, $user);
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
return $this->transformReportFullView($automatedReport);
}
public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array
{
if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {
throw new InvalidArgumentException(
'This report is missing a saved search or prompt. ' .
'Edit the report to complete the setup before enabling it.'
);
}
$this->automatedReportsRepository->update($report, ['status' => $status]);
return $this->transformReportFullView($report->fresh());
}
/**
* Validate and transform data for Ask Jiminny reports.
*/
private function validateAskJiminnyReportData(array $data, User $user): array
{
// Validate name
$name = trim($data['report_name'] ?? '');
if (empty($name)) {
throw new InvalidArgumentException('Report name is required');
}
if (mb_strlen($name) > 50) {
throw new InvalidArgumentException('Report name must be 50 characters or less');
}
// Validate frequency (only daily, weekly, monthly for Ask Jiminny)
$frequency = $data['frequency'] ?? null;
$askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];
if (! in_array($frequency, $askJiminnyFrequencies, true)) {
throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');
}
// Validate expiration date
$expiresAt = $data['expires_on'] ?? null;
if (empty($expiresAt)) {
throw new InvalidArgumentException('Expiration date is required');
}
try {
$expiresAtDate = Carbon::parse($expiresAt);
} catch (InvalidFormatException $e) {
throw new InvalidArgumentException('Expiration date format is invalid');
}
$maxExpiration = Carbon::now()->addYear()->endOfDay();
if ($expiresAtDate->gt($maxExpiration)) {
throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');
}
if ($expiresAtDate->isPast()) {
throw new InvalidArgumentException('Expiration date cannot be in the past');
}
// Validate saved search
$activitySearchId = $data['saved_search'] ?? null;
if (empty($activitySearchId)) {
throw new InvalidArgumentException('Saved search is required');
}
$savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);
if (! $savedSearch) {
throw new InvalidArgumentException('Saved search not found or does not belong to you');
}
// Validate saved prompt
$askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;
if (empty($askAnythingPromptId)) {
throw new InvalidArgumentException('Ask Jiminny prompt is required');
}
$prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);
if (! $prompt) {
throw new InvalidArgumentException('Ask Jiminny prompt not found');
}
// Validate status
$status = $data['enabled'] ?? false;
$recipientUserIds = [$user->getId()];
if (! empty($data['share_users'])) {
$sharedUserIds = $this->validateAndGetUserIdsByTeam(
$user->team,
(array) $data['share_users']
);
$recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);
}
$sharedGroupIds = [];
if (! empty($data['share_teams'])) {
$sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);
}
$recipientUserIds = array_values(array_unique($recipientUserIds));
return [
'team_id' => $user->getTeamId(),
'type' => self::TYPE_ASK_JIMINNY,
'status' => (bool) $status,
'frequency' => $frequency,
'custom_name' => $name,
'activity_search_id' => $savedSearch->getId(),
'ask_anything_prompt_id' => $prompt->getId(),
'expires_at' => $expiresAtDate->toDateString(),
'media_types' => [self::MEDIA_TYPE_PDF],
'call_types' => [],
'recipients' => ['users' => $recipientUserIds],
'groups' => $sharedGroupIds,
];
}
public static function getAskJiminnyFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::ASK_JIMINNY_FREQUENCIES);
}
public function getAskJiminnyReportFilters(User $user): array
{
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
return [
[
'id' => 'prompt',
'label' => 'Prompt',
'options' => $prompts,
],
[
'id' => 'saved_search',
'label' => 'Saved Search',
'options' => $savedSearches,
],
];
}
public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array
{
$team = $user->getTeam();
$userTimezone = $user->getTimezone();
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
$teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [
'id' => $group->getUuid(),
'name' => $group->getName(),
])->values()->all();
$shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];
$sharedTeamsValue = [];
$sharedUsersValue = [];
if ($report) {
$sharedTeamsValue = $this->transformGroups($team, $report->getGroups());
$recipientUserIds = $report->getRecipients()['users'] ?? [];
$creatorId = $report->getAttribute('created_by');
$sharedUserIds = array_values(array_filter(
$recipientUserIds,
static fn ($id) => $id !== $creatorId
));
$sharedUsersValue = collect($sharedUserIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (User $u) => [
'id' => $u->getUuid(),
'name' => $u->getName(),
])
->values()
->all();
}
return [
'fields' => [
[
'id' => 'enabled',
'inputType' => InputTypeEnum::TOGGLE,
'label' => '',
'value' => $report?->getStatus() ?? false,
],
[
'id' => 'report_name',
'inputType' => InputTypeEnum::TEXT,
'label' => 'Name',
'placeholder' => 'Enter name',
'required' => true,
'validation' => ['maxLength' => 50],
'value' => $report?->getCustomName() ?? '',
...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects<br/>Some incoming commits are not fetched<br/>","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,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskAnythingPromptServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskAnythingPromptServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskAnythingPromptServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"100","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"34","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"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\\Services\\Kiosk\\AutomatedReports;\n\nuse Carbon\\CarbonImmutable;\nuse Carbon\\CarbonInterface;\nuse Carbon\\Exceptions\\InvalidFormatException;\nuse DateTime;\nuse DateTimeInterface;\nuse DateTimeZone;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\InputTypeEnum;\nuse Jiminny\\Component\\AskAnything\\AskAnythingPromptService;\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Contracts\\Repositories\\PlaybookCategoryRepository;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\ModelNotFoundException;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Partner;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Repositories\\GroupRepository;\nuse Jiminny\\Repositories\\SearchRepository;\nuse Jiminny\\Repositories\\StageRepository;\nuse Throwable;\n\nclass AutomatedReportsService\n{\n public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';\n public const string TYPE_ASK_JIMINNY = 'ask_jiminny';\n\n /**\n * Standard report types (used by kiosk for existing automated reports).\n */\n // @TODO this will add filter, however if we need to control feature by FF we need conditional logic\n public const array TYPES = [\n ['id' => 'exec_summary', 'name' => 'Exec Summary'],\n ['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],\n ['id' => 'product_feedback', 'name' => 'Product Feedback'],\n ['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],\n// ['id' => 'questions', 'name' => 'Questions'],\n// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],\n ];\n\n public const array ALL_TYPES = [\n ...self::TYPES,\n ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],\n ];\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\n /**\n * Frequencies for standard (non-Ask Jiminny) reports.\n */\n public const array FREQUENCIES = [\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n\n /**\n * Frequencies for Ask Jiminny reports.\n */\n public const array ASK_JIMINNY_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ];\n\n public const string MEDIA_TYPE_PDF = 'pdf';\n public const string MEDIA_TYPE_PODCAST = 'podcast';\n public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];\n public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];\n public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];\n public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];\n\n public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];\n public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];\n public const int SENT_REPORT_AT_HOURS = 5;\n public const string PDF_KEY = 'pdf';\n public const string AUDIO_KEY = 'audio';\n\n private const array ALL_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n private const string S3_DIR = 'reports';\n private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];\n private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];\n\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly GroupRepository $groupRepository,\n private readonly UserRepository $userRepository,\n private readonly StageRepository $stageRepository,\n private readonly DealStagesService $dealStagesService,\n private readonly RecipientsService $recipientsService,\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly Webhook $webhookService,\n private readonly BusDispatcher $dispatcher,\n private readonly ActivityTypeService $activityTypeService,\n private readonly PlaybookCategoryRepository $playbookCategoryRepository,\n private readonly AskAnythingPromptService $askAnythingPromptService,\n private readonly SearchRepository $activitySearchRepository,\n private readonly AskAnythingRepository $askAnythingRepository,\n ) {\n }\n\n public static function getTypes(): array\n {\n $types = self::TYPES;\n\n return array_map(static function ($type) {\n return $type['id'];\n }, $types);\n }\n\n public static function getCallTypes(): array\n {\n return array_map(static function ($callType) {\n return $callType['id'];\n }, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);\n }\n\n public static function getFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::FREQUENCIES);\n }\n\n // front-facing structure\n public function getReportEnabledFieldData(bool $value = false): array\n {\n return [\n 'id' => 'report_enabled',\n 'label' => '',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'value' => $value,\n ];\n }\n\n // Organizations = Teams\n public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array\n {\n $options = $this->getTeams(partner: $partner);\n\n if ($shortVersion) {\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value,\n 'dependencies' => [\n 'teams',\n 'deal_stage_at_call',\n 'current_deal_stage',\n 'recipients',\n ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,\n ],\n 'dependsOn' => [],\n ];\n }\n\n // Teams = Groups\n public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array\n {\n if ($shortVersion) {\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value, // value should be an array of objects {id, name}\n 'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],\n 'dependsOn' => [],\n ];\n }\n\n public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array\n {\n $types = [];\n if ($team instanceof Team) {\n if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n $types = self::TYPES;\n }\n if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {\n $types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];\n }\n } else {\n $types = self::TYPES;\n }\n\n if ($shortVersion) {\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'options' => $types,\n ];\n }\n\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $types,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getFrequencyFieldData(?string $value = null): array\n {\n return [\n 'id' => 'frequency',\n 'label' => 'Frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::FREQUENCIES,\n 'value' => $value,\n 'dependencies' => ['period'],\n 'dependsOn' => [],\n ];\n }\n\n public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array\n {\n return [\n 'id' => 'period',\n 'label' => 'Select one-off period',\n 'inputType' => InputTypeEnum::DATE_RANGE,\n 'required' => true,\n 'placeholder' => 'Select',\n 'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],\n 'queryParams' => [\n 'startDate' => 'start_date_period',\n 'endDate' => 'end_date_period',\n ],\n 'dependencies' => [],\n 'dependsOn' => ['frequency'],\n ];\n }\n\n public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array\n {\n return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);\n }\n\n public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);\n }\n\n public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);\n }\n\n public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'deal_value',\n 'label' => 'Deal Value',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_deal_value',\n 'max' => 'max_deal_value',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array\n {\n $value = [];\n $conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;\n $dialerOn && $value[] = self::CALL_TYPE_DIALER;\n\n return [\n 'id' => 'call_type',\n 'label' => 'Call Type',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => [\n self::CALL_TYPE_CONFERENCE,\n self::CALL_TYPE_DIALER,\n ],\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getMediaTypeFieldData(?AutomatedReport $report = null): array\n {\n $value = [];\n\n if ($report) {\n $value = $this->transformMediaTypes($report);\n }\n\n return [\n 'id' => 'media_types',\n 'label' => 'Export as',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => self::MEDIA_TYPE_OBJECTS,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'call_duration',\n 'label' => 'Call Duration',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_call_duration',\n 'max' => 'max_call_duration',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getRecipientsFieldData(?Team $team = null, array $value = []): array\n {\n return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);\n }\n\n public function getJiminnyRecipientsFieldData(array $value = []): array\n {\n return $this->recipientsService->getJiminnyRecipientsFieldData($value);\n }\n\n public function getAdditionalPromptInputFieldData(?string $value = null): array\n {\n return [\n 'id' => 'additional_prompt_input',\n 'label' => 'Special requirements',\n 'inputType' => InputTypeEnum::TEXTAREA,\n 'required' => false,\n 'placeholder' => 'What should be the focus of the report?',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCustomReportNameFieldData(?string $value = null): array\n {\n return [\n 'id' => 'custom_name',\n 'label' => 'Custom report name',\n 'inputType' => InputTypeEnum::TEXT,\n 'required' => false,\n 'placeholder' => 'Enter custom name',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n // data providers\n public function getTeams(?Partner $partner = null): array\n {\n $teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);\n\n $teamData = [];\n foreach ($teams as $team) {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n continue;\n }\n\n $teamData[] = $this->transformTeam($team);\n }\n\n return $teamData;\n }\n\n public function getTeamGroups(string $teamUuid): array\n {\n $data = [];\n $team = $this->getTeam($teamUuid);\n\n if ($team !== null) {\n $groups = $team->groups()->get();\n\n foreach ($groups as $group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array\n {\n $data = [];\n $teams = $this->getTeams(partner: $partner);\n\n foreach ($teams as $team) {\n if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {\n continue;\n }\n\n $data[] = [\n 'label' => $team['name'],\n 'groups' => $this->getTeamGroups($team['id']),\n ];\n }\n\n return $data;\n }\n\n public function getTeam(string $teamUuid): ?Team\n {\n return $this->teamRepository->idOrUuid($teamUuid);\n }\n\n public function getTeamById(int $teamId): ?Team\n {\n return $this->teamRepository->find($teamId);\n }\n\n public function getGroupsUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportGroups = $report->getGroups();\n foreach ($reportGroups as $groupId) {\n if ($group = $this->groupRepository->find($groupId)) {\n $uuids[] = $group->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getPlaybookCategoriesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $playbookCategories = $report->getPlaybookCategories();\n foreach ($playbookCategories as $id) {\n if ($category = $this->playbookCategoryRepository->find($id)) {\n $uuids[] = $category->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getDealAtCallStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getDealAtCallStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getCurrentDealStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getCurrentDealStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getRecipients());\n }\n\n public function getJiminnyUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function extractUserUuids(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => $user->getUuid())\n ->values()\n ->all();\n }\n\n // get mail data\n public function getRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getRecipients());\n }\n\n /**\n * @return array<UserContract>\n */\n public function getRecipientUserObjects(AutomatedReport $report): array\n {\n $userIds = $report->getRecipients()['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->values()\n ->all();\n }\n\n private function getJiminnyRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function buildRecipientUsers(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => [\n 'email' => $user->getEmailAddress(),\n 'name' => $user->getName(),\n 'timezone' => $user->getTimezone()->getName(),\n ])\n ->values()\n ->all();\n }\n\n public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array\n {\n if ($report->isAskJiminnyReport()) {\n $recipients = $this->resolveAskJiminnyRecipients($report);\n } else {\n $recipients = $this->getRecipientUsers($report);\n if ($includeJiminny) {\n $recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));\n }\n }\n\n $emails = [];\n\n return array_values(array_filter(\n $recipients,\n static function ($recipient) use (&$emails) {\n if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {\n return false;\n }\n $emails[] = $recipient['email'];\n\n return true;\n }\n ));\n }\n\n private function resolveAskJiminnyRecipients(AutomatedReport $report): array\n {\n $recipients = [];\n\n $creator = $report->getCreator();\n if ($creator !== null) {\n $recipients[] = [\n 'email' => $creator->getEmailAddress(),\n 'name' => $creator->getName(),\n 'timezone' => $creator->getTimezone()->getName(),\n ];\n }\n\n return array_merge(\n $recipients,\n $this->buildRecipientUsers($report->getRecipients()),\n $this->getGroupRecipientUsers($report),\n );\n }\n\n private function getGroupRecipientUsers(AutomatedReport $report): array\n {\n $users = [];\n foreach ($report->getGroups() as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group === null) {\n continue;\n }\n foreach ($group->getMembers() as $member) {\n $users[] = [\n 'email' => $member->getEmailAddress(),\n 'name' => $member->getName(),\n 'timezone' => $member->getTimezone()->getName(),\n ];\n }\n }\n\n return $users;\n }\n\n public function getReportTypeName(AutomatedReportResult $report): string\n {\n $type = $report->getReport()->getType();\n\n $getType = $this->transformReportType($type);\n\n return $getType['name'];\n }\n\n public function getReportPeriodName(AutomatedReportResult $report): string\n {\n $from = $report->getFromDate();\n $to = $report->getToDate();\n $frequency = $report->getReport()->getFrequency();\n\n if ($from === null || $to === null) {\n if (! $report->getReport()->isAskJiminnyReport()) {\n $invalidPeriod = $from === null ? 'from' : 'to';\n\n throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);\n }\n\n $timezone = $report->getReport()->getCreator()?->getTimezone();\n $period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);\n $from = $period['fromDate'];\n $to = $period['toDate'];\n }\n\n return $this->formatReportPeriodName($frequency, $from, $to);\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 getReportTeamsName(AutomatedReportResult $report): string\n {\n $groups = $report->getGroups();\n\n if (empty($groups)) {\n return 'All';\n }\n\n // Get group names from repository\n $groupNames = [];\n foreach ($groups as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group) {\n $groupNames[] = $group->getName();\n }\n }\n\n if (count($groupNames) === 1) {\n // Single team format\n $teamsName = $groupNames[0];\n } else {\n // Multiple teams format\n $teamsName = implode(', ', $groupNames);\n }\n\n return $teamsName;\n }\n\n public function getReportFileName(AutomatedReportResult $report): string\n {\n $customName = $report->getReport()->getCustomName();\n $periodName = $this->getReportPeriodName($report);\n $filenameSuffix = $this->getFilenameSuffix($report);\n\n if ($customName) {\n if ($filenameSuffix) {\n $customName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$customName} - {$periodName}\");\n }\n\n $baseName = $this->getReportTypeName($report);\n\n if ($filenameSuffix) {\n $baseName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}\");\n }\n\n public function getReportFileNameWithExtension(AutomatedReportResult $result): string\n {\n $extension = $this->getMediaTypeMetadata($result)['extension'];\n\n return $this->getReportFileName($result) . '.' . $extension;\n }\n\n public function sanitizeFileName(string $fileName): string\n {\n return str_replace(['/', '\\\\'], '-', $fileName);\n }\n\n public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool\n {\n $recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);\n if (in_array($user->getId(), $recipientIds, true)) {\n return true;\n }\n\n if ($report->isAskJiminnyReport()) {\n $groupId = $user->getGroupId();\n if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {\n return true;\n }\n }\n\n return false;\n }\n\n public function transformReportResults(Collection $automatedReportResults): array\n {\n $data = [];\n foreach ($automatedReportResults as $automatedReportResult) {\n /** @var AutomatedReportResult $automatedReportResult */\n\n $report = $automatedReportResult->getReport();\n\n $createdBy = $report->getCreator();\n $creator = [\n 'id' => $createdBy?->getUuid(),\n 'name' => $createdBy?->getName(),\n 'email' => $createdBy?->getEmailAddress(),\n 'photoUrl' => $createdBy?->getPhotoUrl(),\n ];\n\n $data[] = [\n 'id' => $automatedReportResult->getUuid(),\n 'name' => $automatedReportResult->getName(),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n 'recipients' => $this->buildRecipients($report),\n 'report_type' => $this->transformReportType($report->getType()),\n 'media_type' => $automatedReportResult->getMediaType(),\n 'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),\n 'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),\n 'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),\n 'creator' => $creator,\n ];\n }\n\n return $data;\n }\n\n private function buildRecipients(AutomatedReport $report): array\n {\n $creatorUuid = $report->getCreator()?->getUuid();\n\n $recipients = array_values(array_filter(\n $this->transformRecipients($report->getRecipients()),\n static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,\n ));\n\n if (! $report->isAskJiminnyReport()) {\n return $recipients;\n }\n\n return [\n ...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),\n ...$recipients,\n ];\n }\n\n public function hasCallTypeConference(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);\n }\n\n public function hasCallTypeDialer(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);\n }\n\n // transformers\n private function transformTeam(Team $team): array\n {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n return [];\n }\n\n return [\n 'id' => $team->getUuid(),\n 'name' => $team->getName(),\n ];\n }\n\n private function transformReportFullView(AutomatedReport $report): array\n {\n $base = $this->transformReportBase($report);\n\n return $report->getType() === self::TYPE_ASK_JIMINNY\n ? $base + $this->transformAskJiminnyFields($report)\n : $base + $this->transformStandardReportFields($report);\n }\n\n private function transformReportBase(AutomatedReport $report): array\n {\n return [\n 'id' => $report->getUuid(),\n 'organization' => $this->transformOrganization(team: $report->getTeam()),\n 'report_type' => $this->transformReportType($report->getType()),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n ];\n }\n\n private function transformStandardReportFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n\n return [\n 'report_enabled' => $report->getStatus(),\n 'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),\n 'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),\n 'deal_value_min' => $report->getDealValueMin(),\n 'deal_value_max' => $report->getDealValueMax(),\n 'call_types' => $this->transformCallType($report->getCallTypes()),\n 'media_types' => $this->transformMediaTypes($report),\n 'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),\n 'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),\n 'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),\n 'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),\n 'recipients' => $this->transformRecipients($report->getRecipients()),\n 'created_by' => $this->transformCreator($report->getCreator()),\n 'additional_prompt_input' => $report->getAdditionalPromptInput(),\n 'custom_name' => $report->getCustomName(),\n 'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),\n 'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),\n 'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),\n ];\n }\n\n private function transformAskJiminnyFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n $creatorId = $report->getAttribute('created_by');\n $explicitUserIds = array_values(array_filter(\n $report->getRecipients()['users'] ?? [],\n static fn ($id) => $id !== $creatorId\n ));\n\n return [\n 'report_name' => $report->getCustomName(),\n 'enabled' => $report->getStatus(),\n 'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),\n 'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),\n 'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),\n 'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),\n ];\n }\n\n private function transformOrganization(?Team $team): array\n {\n return [\n 'id' => $team?->getUuid(),\n 'name' => $team?->getName(),\n ];\n }\n\n private function transformReportType(string $type): array\n {\n foreach (self::ALL_TYPES as $typeItem) {\n if ($typeItem['id'] === $type) {\n return $typeItem;\n }\n }\n\n return [];\n }\n\n private function transformCallType(array $types): array\n {\n $result = [];\n $callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];\n\n foreach ($types as $type) {\n foreach ($callTypes as $callTypeItem) {\n if ($callTypeItem['id'] === $type) {\n $result[] = $callTypeItem;\n\n break;\n }\n }\n }\n\n return $result;\n }\n\n private function transformMediaTypes(AutomatedReport $report): array\n {\n $values = [];\n\n foreach ($report->getMediaTypes() as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n continue;\n }\n\n $values[] = match ($mediaType) {\n self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,\n self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,\n };\n }\n\n return $values;\n }\n\n private function transformFrequency(string $frequency): array\n {\n foreach (self::ALL_FREQUENCIES as $frequencyItem) {\n if ($frequencyItem['id'] === $frequency) {\n return $frequencyItem;\n }\n }\n\n return [];\n }\n\n public function transformDurationToMinutes(?int $duration): ?int\n {\n if (! $duration) {\n return null;\n }\n\n return (int) ($duration / 60);\n }\n\n private function transformGroups(?Team $team, array $groupsIds): array\n {\n if (empty($groupsIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($groupsIds as $groupId) {\n $group = $team->groups()->where('id', $groupId)->first();\n\n if ($group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n 'photoUrl' => $group->getPhotoUrl(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformStages(?Team $team, array $stagesIds): array\n {\n if (empty($stagesIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($stagesIds as $stageId) {\n $stage = $team->stages()->where('id', $stageId)->first();\n\n if ($stage) {\n $data[] = [\n 'id' => $stage->getUuid(),\n 'name' => $stage->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformRecipients(array $recipients): array\n {\n $users = [];\n foreach ($recipients['users'] ?? [] as $userId) {\n $users[] = $this->transformUser($userId);\n }\n\n return $users;\n }\n\n private function transformCreator(?User $user): ?array\n {\n if ($user === null) {\n return null;\n }\n\n return $this->transformUser($user->getId());\n }\n\n private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array\n {\n if ($prompt === null) {\n return null;\n }\n\n return [\n 'id' => $prompt->getUuid(),\n 'name' => $prompt->getTitle(),\n ];\n }\n\n private function transformSafeSearch(?Search $search): ?array\n {\n if ($search === null) {\n return null;\n }\n\n return [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ];\n }\n\n private function transformUser(int $userId): array\n {\n /* @var ?User $user */\n $user = $this->userRepository->find($userId);\n\n return [\n 'id' => $user?->getUuid(),\n 'name' => $user?->getName(),\n 'email' => $user?->getEmailAddress(),\n 'photoUrl' => $user?->getPhotoUrl(),\n ];\n }\n\n public function create(array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $validatedData['created_by'] = auth()->id();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function update(string $uuid, array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $report = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $report) {\n throw new InvalidArgumentException('Report not found');\n }\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Create an Ask Jiminny report.\n */\n public function createAskJiminnyReport(array $data, User $creator): array\n {\n $validatedData = $this->validateAskJiminnyReportData($data, $creator);\n $validatedData['created_by'] = $creator->getId();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Update an Ask Jiminny report.\n */\n public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array\n {\n if (! $report->isAskJiminnyReport()) {\n throw new InvalidArgumentException('Report is not an Ask Jiminny report');\n }\n\n $validatedData = $this->validateAskJiminnyReportData($data, $user);\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array\n {\n if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {\n throw new InvalidArgumentException(\n 'This report is missing a saved search or prompt. ' .\n 'Edit the report to complete the setup before enabling it.'\n );\n }\n\n $this->automatedReportsRepository->update($report, ['status' => $status]);\n\n return $this->transformReportFullView($report->fresh());\n }\n\n /**\n * Validate and transform data for Ask Jiminny reports.\n */\n private function validateAskJiminnyReportData(array $data, User $user): array\n {\n // Validate name\n $name = trim($data['report_name'] ?? '');\n if (empty($name)) {\n throw new InvalidArgumentException('Report name is required');\n }\n if (mb_strlen($name) > 50) {\n throw new InvalidArgumentException('Report name must be 50 characters or less');\n }\n\n // Validate frequency (only daily, weekly, monthly for Ask Jiminny)\n $frequency = $data['frequency'] ?? null;\n $askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];\n if (! in_array($frequency, $askJiminnyFrequencies, true)) {\n throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');\n }\n\n // Validate expiration date\n $expiresAt = $data['expires_on'] ?? null;\n if (empty($expiresAt)) {\n throw new InvalidArgumentException('Expiration date is required');\n }\n\n try {\n $expiresAtDate = Carbon::parse($expiresAt);\n } catch (InvalidFormatException $e) {\n throw new InvalidArgumentException('Expiration date format is invalid');\n }\n $maxExpiration = Carbon::now()->addYear()->endOfDay();\n if ($expiresAtDate->gt($maxExpiration)) {\n throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');\n }\n if ($expiresAtDate->isPast()) {\n throw new InvalidArgumentException('Expiration date cannot be in the past');\n }\n\n // Validate saved search\n $activitySearchId = $data['saved_search'] ?? null;\n if (empty($activitySearchId)) {\n throw new InvalidArgumentException('Saved search is required');\n }\n $savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);\n if (! $savedSearch) {\n throw new InvalidArgumentException('Saved search not found or does not belong to you');\n }\n\n // Validate saved prompt\n $askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;\n if (empty($askAnythingPromptId)) {\n throw new InvalidArgumentException('Ask Jiminny prompt is required');\n }\n $prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);\n if (! $prompt) {\n throw new InvalidArgumentException('Ask Jiminny prompt not found');\n }\n\n // Validate status\n $status = $data['enabled'] ?? false;\n\n $recipientUserIds = [$user->getId()];\n\n if (! empty($data['share_users'])) {\n $sharedUserIds = $this->validateAndGetUserIdsByTeam(\n $user->team,\n (array) $data['share_users']\n );\n $recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);\n }\n\n $sharedGroupIds = [];\n if (! empty($data['share_teams'])) {\n $sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);\n }\n\n $recipientUserIds = array_values(array_unique($recipientUserIds));\n\n return [\n 'team_id' => $user->getTeamId(),\n 'type' => self::TYPE_ASK_JIMINNY,\n 'status' => (bool) $status,\n 'frequency' => $frequency,\n 'custom_name' => $name,\n 'activity_search_id' => $savedSearch->getId(),\n 'ask_anything_prompt_id' => $prompt->getId(),\n 'expires_at' => $expiresAtDate->toDateString(),\n 'media_types' => [self::MEDIA_TYPE_PDF],\n 'call_types' => [],\n 'recipients' => ['users' => $recipientUserIds],\n 'groups' => $sharedGroupIds,\n ];\n }\n\n public static function getAskJiminnyFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::ASK_JIMINNY_FREQUENCIES);\n }\n\n public function getAskJiminnyReportFilters(User $user): array\n {\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n return [\n [\n 'id' => 'prompt',\n 'label' => 'Prompt',\n 'options' => $prompts,\n ],\n [\n 'id' => 'saved_search',\n 'label' => 'Saved Search',\n 'options' => $savedSearches,\n ],\n ];\n }\n\n public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array\n {\n $team = $user->getTeam();\n $userTimezone = $user->getTimezone();\n\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n $teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ])->values()->all();\n\n $shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];\n\n $sharedTeamsValue = [];\n $sharedUsersValue = [];\n if ($report) {\n $sharedTeamsValue = $this->transformGroups($team, $report->getGroups());\n\n $recipientUserIds = $report->getRecipients()['users'] ?? [];\n $creatorId = $report->getAttribute('created_by');\n $sharedUserIds = array_values(array_filter(\n $recipientUserIds,\n static fn ($id) => $id !== $creatorId\n ));\n $sharedUsersValue = collect($sharedUserIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (User $u) => [\n 'id' => $u->getUuid(),\n 'name' => $u->getName(),\n ])\n ->values()\n ->all();\n }\n\n return [\n 'fields' => [\n [\n 'id' => 'enabled',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'label' => '',\n 'value' => $report?->getStatus() ?? false,\n ],\n [\n 'id' => 'report_name',\n 'inputType' => InputTypeEnum::TEXT,\n 'label' => 'Name',\n 'placeholder' => 'Enter name',\n 'required' => true,\n 'validation' => ['maxLength' => 50],\n 'value' => $report?->getCustomName() ?? '',\n ],\n [\n 'id' => 'frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Frequency',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::ASK_JIMINNY_FREQUENCIES,\n 'value' => $report ? $this->transformFrequency($report->getFrequency()) : null,\n ],\n [\n 'id' => 'expires_on',\n 'inputType' => InputTypeEnum::DATE,\n 'label' => 'Expires on',\n 'required' => true,\n 'placeholder' => 'Select',\n 'validation' => [\n 'minDate' => now($userTimezone)->toDateString(),\n 'maxDate' => now($userTimezone)->addYear()->toDateString(),\n ],\n 'value' => $report?->getExpiresAt()?->toDateString(),\n ],\n [\n 'id' => 'share_teams',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team',\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $teamGroups,\n 'value' => $sharedTeamsValue,\n ],\n [\n 'id' => 'share_users',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team member',\n 'required' => false,\n 'placeholder' => 'Select',\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'options' => $shareUsers,\n 'value' => $sharedUsersValue,\n ],\n [\n 'id' => 'saved_search',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Saved search',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $savedSearches,\n 'value' => $report && $report->getSavedSearch() ? [\n 'id' => $report->getSavedSearch()->getUuid(),\n 'name' => $report->getSavedSearch()->getName(),\n ] : null,\n ],\n [\n 'id' => 'ask_jiminny_prompt',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Ask Jiminny prompt',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $prompts,\n 'value' => $report && $report->getAskAnythingPrompt() ? [\n 'id' => $report->getAskAnythingPrompt()->getUuid(),\n 'name' => $report->getAskAnythingPrompt()->getTitle(),\n ] : null,\n ],\n ],\n ];\n }\n\n private function updateResultNames(AutomatedReport $automatedReport): void\n {\n $results = $this->automatedReportsRepository->getResultsByReport($automatedReport);\n\n foreach ($results as $result) {\n $result->update(['name' => $this->getReportFileName($result)]);\n }\n }\n\n public function updateStatus(string $uuid, array $data): array\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $automatedReport->update([\n 'status' => $status,\n ]);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n private function generateOneOffReport(AutomatedReport $automatedReport): void\n {\n // the scheduler handles all the other frequency types\n if ($automatedReport->getStatus() === false || $automatedReport->getFrequency() !== self::FREQUENCY_ONE_OFF) {\n return;\n }\n\n $this->dispatcher->dispatch(new RequestGenerateReportJob($automatedReport->getUuid()));\n }\n\n public function getReport(string $uuid, ?Partner $partner = null): AutomatedReport\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n if ($partner !== null && ! $partner->isDefaultPartner() && $automatedReport->team->partner_id !== $partner->getId()) {\n throw new ModelNotFoundException('Report not found');\n }\n\n return $automatedReport;\n }\n\n public function get(string $uuid, ?Partner $partner = null): array\n {\n $automatedReport = $this->getReport($uuid, $partner);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function list(string $sortColumn = 'created_at', string $sortDirection = 'desc', ?Partner $partner = null): array\n {\n $results = [];\n $collection = $this->automatedReportsRepository->getAllStandardReports($sortColumn, $sortDirection, $partner);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function listAskJiminnyReports(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): array {\n $results = [];\n $collection = $this->automatedReportsRepository->getAskJiminnyReportsByUser($user, $sortColumn, $sortDirection);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function delete(string $uuid): void\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $automatedReport->delete();\n }\n\n public function createReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n return $this->automatedReportsRepository->createResult(\n array_merge(\n [\n 'report_id' => $automatedReport->getId(),\n 'status' => AutomatedReportResult::STATUS_DEFAULT,\n ],\n $data\n )\n );\n }\n\n public function getOrCreateReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n $existing = $this->automatedReportsRepository->findLatestSameDayDefaultOrFailedResult($automatedReport);\n\n if ($existing !== null) {\n $existing->update(['status' => AutomatedReportResult::STATUS_DEFAULT]);\n\n return $existing;\n }\n\n return $this->createReportResult($automatedReport, $data);\n }\n\n public function getReportResult(string $resultUuid): AutomatedReportResult\n {\n $report = $this->automatedReportsRepository->findResultByUuid($resultUuid);\n\n if (! $report) {\n throw new ModelNotFoundException('Report Result not found');\n }\n\n return $report;\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return $this->automatedReportsRepository->findChildResult($result, $type);\n }\n\n // prophet API calls\n /**\n * @throws ApplicationException\n */\n public function getGenerateReportPayload(AutomatedReport $automatedReport, string $reportResultUuid): array\n {\n $period = $this->calculateFromAndToDate($automatedReport);\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n return [\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResultUuid,\n 'report_type' => $automatedReport->getType(),\n 'media_types' => $automatedReport->getMediaTypes(),\n 'from_date' => $fromDate->startOfDay()->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->endOfDay()->format(DateTimeInterface::RFC3339),\n 'group_ids' => $automatedReport->getGroups(),\n 'call_deal_stage' => $automatedReport->getDealAtCallStages(),\n 'current_deal_stage' => $automatedReport->getCurrentDealStages(),\n 'deal_min_value' => $automatedReport->getDealValueMin(),\n 'deal_max_value' => $automatedReport->getDealValueMax(),\n 'call_types' => $automatedReport->getCallTypes(),\n 'call_duration_min_seconds' => $automatedReport->getCallDurationMin(),\n 'call_duration_max_seconds' => $automatedReport->getCallDurationMax(),\n 'special_requirements' => $automatedReport->getAdditionalPromptInput(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->formatReportPeriodName(\n $automatedReport->getFrequency(),\n $fromDate,\n $toDate,\n ),\n 'playbook_categories' => $automatedReport->getPlaybookCategories(),\n 'custom_name' => $automatedReport->getCustomName(),\n ];\n }\n\n // $inputPayload - FE payload structure\n public function getActivitiesCountPayload(array $inputPayload): array\n {\n // Use validateAndTransformData to validate and normalize input\n $validatedData = $this->validateAndTransformData($inputPayload);\n $period = $this->calculateFromAndToDatePeriod(\n $validatedData['frequency'],\n Carbon::parse($validatedData['from']),\n Carbon::parse($validatedData['to']),\n );\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n // Create payload similar to getGenerateReportPayload\n return [\n 'team_id' => $validatedData['team_id'],\n 'group_ids' => $validatedData['groups'] ?? [],\n 'report_type' => $validatedData['type'],\n 'from_date' => $fromDate->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->format(DateTimeInterface::RFC3339),\n 'call_deal_stage' => $validatedData['deal_at_call_stages'] ?? [],\n 'current_deal_stage' => $validatedData['current_deal_stages'] ?? [],\n 'deal_min_value' => $validatedData['deal_value_min'] ?? null,\n 'deal_max_value' => $validatedData['deal_value_max'] ?? null,\n 'call_types' => $validatedData['call_types'],\n 'call_duration_min_seconds' => $validatedData['call_duration_min'] ?? null,\n 'call_duration_max_seconds' => $validatedData['call_duration_max'] ?? null,\n 'special_requirements' => $validatedData['additional_prompt_input'] ?? null,\n 'playbook_categories' => $validatedData['playbook_categories'] ?? [],\n 'request_id' => null,\n 'callback_url' => null,\n ];\n }\n\n public function shouldSendReport(array $users, ?CarbonInterface $generatedAt = null): bool\n {\n if (empty($users)) {\n return false;\n }\n\n $earliestTz = collect($users)\n ->mapWithKeys(function (array $user) {\n $tz = new DateTimeZone($user['timezone']);\n $nowUtc = new DateTime('now', new DateTimeZone('UTC'));\n $offset = $tz->getOffset($nowUtc);\n\n return [$user['timezone'] => $offset];\n })\n ->sortDesc()\n ->keys()\n ->first();\n\n $now = Carbon::now($earliestTz);\n $isScheduledTime = (int) $now->format('H') === self::SENT_REPORT_AT_HOURS;\n\n if ($isScheduledTime) {\n return true;\n }\n\n return $this->hasPassedScheduledTime($generatedAt, $earliestTz);\n }\n\n public function hasPassedScheduledTime(?CarbonInterface $generatedAt, string $timezone): bool\n {\n if ($generatedAt === null) {\n return false;\n }\n\n $now = Carbon::now($timezone);\n $scheduledTime = $now->copy()->setTime(self::SENT_REPORT_AT_HOURS, 0, 0);\n\n if ($now->hour < self::SENT_REPORT_AT_HOURS) {\n $scheduledTime = $scheduledTime->subDay();\n }\n\n $scheduledTimeUtc = $scheduledTime->copy()->utc();\n $generatedAtUtc = $generatedAt->copy()->utc();\n $nowUtc = $now->copy()->utc();\n\n return $generatedAtUtc->lt($scheduledTimeUtc) && $nowUtc->gt($scheduledTimeUtc);\n }\n\n public function calculateFromAndToDatePeriod(\n string $frequency,\n ?Carbon $fromDate = null,\n ?Carbon $toDate = null,\n DateTimeZone|string|null $timezone = 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($timezone);\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()->subWeek()->startOfWeek(CarbonInterface::MONDAY),\n 'toDate' => $now->copy()->subWeek()->endOfWeek(CarbonInterface::SUNDAY),\n ],\n self::FREQUENCY_MONTHLY => [\n 'fromDate' => $now->copy()->subMonthNoOverflow()->startOfMonth(),\n 'toDate' => $now->copy()->subMonthNoOverflow()->endOfMonth(),\n ],\n self::FREQUENCY_QUARTERLY => [\n 'fromDate' => $now->copy()->subQuarterNoOverflow()->startOfQuarter(),\n 'toDate' => $now->copy()->subQuarterNoOverflow()->endOfQuarter(),\n ],\n default => throw new InvalidArgumentException(\"Unsupported frequency: {$frequency}\"),\n };\n }\n\n private function calculateFromAndToDate(AutomatedReport $automatedReport): array\n {\n return $this->calculateFromAndToDatePeriod(\n $automatedReport->getFrequency(),\n $automatedReport->getFrom(),\n $automatedReport->getTo()\n );\n }\n\n public function getAskJiminnyGenerateReportPayload(\n AutomatedReport $automatedReport,\n AutomatedReportResult $reportResult,\n array $activityIds,\n ): array {\n return [\n 'user_question' => $automatedReport->getAskAnythingPrompt()?->getContent(),\n 'call_ids' => array_map('strval', $activityIds),\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResult->getUuid(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->getReportPeriodName($reportResult),\n 'report_name' => $automatedReport->getCustomName(),\n ];\n }\n\n private function getCallbackUrl(): string\n {\n return $this->webhookService->route('jiminny.webhook.reports.ready');\n }\n\n /**\n * Validate and transform payload data for automated reports\n *\n * @param array $data\n *\n * @throws InvalidArgumentException\n *\n * @return array\n */\n private function validateAndTransformData(array $data): array\n {\n // Validate organization (team) and check feature\n $team = $this->validateOrganization($data['organization'] ?? null);\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $type = $this->validateReportType($data['report_type'] ?? null);\n $frequency = $this->validateFrequency($data['frequency'] ?? null);\n $additionalPromptInput = $this->validateAdditionalPromptInput(\n $data['additional_prompt_input'] ?? null\n );\n $customReportName = $this->validateCustomReportName($data['custom_name'] ?? null);\n\n // Prepare data for the database\n $reportData = [\n 'team_id' => $team->getId(),\n 'type' => $type,\n 'status' => $status,\n 'frequency' => $frequency,\n 'additional_prompt_input' => $additionalPromptInput,\n 'custom_name' => $customReportName,\n ];\n\n // Validate deal values\n $reportData = $this->validateDealValues($data, $reportData);\n\n // Validate date range\n $reportData = $this->validateDateRange($data, $reportData, $frequency);\n\n // Validate call durations\n $reportData = $this->validateCallDurations($data, $reportData);\n\n // Validate call types\n $reportData = $this->validateCallTypes($data, $reportData);\n\n // Validate media types\n $reportData = $this->validateMediaTypes($data, $reportData);\n\n // Validate groups\n if (isset($data['teams'])) {\n $reportData['groups'] = $this->validateAndGetGroupIds($team, $data['teams']);\n }\n\n // Validate deal stages\n $reportData = $this->validateDealStages($data, $reportData, $team, $type);\n\n // Validate playbook categories\n $reportData = $this->validatePlaybookCategories($data, $reportData, $team);\n\n // Validate recipients\n $reportData['recipients'] = [\n 'users' => $this->validateAndGetUserIdsByTeam($team, $data['recipients'] ?? []),\n ];\n\n if (isset($data['jiminny_recipients'])) {\n // Validate Jiminny recipients\n $reportData['jiminny_recipients'] = [\n 'users' => $this->validateAndGetJiminnyUserIds((array) $data['jiminny_recipients']),\n ];\n }\n\n return $reportData;\n }\n\n private function validateDealValues(array $data, array $reportData): array\n {\n if (isset($data['min_deal_value'])) {\n $reportData['deal_value_min'] = (int) $data['min_deal_value'];\n\n if ($reportData['deal_value_min'] > 4294967295 || $reportData['deal_value_min'] < 0) {\n throw new InvalidArgumentException('Min deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_deal_value'])) {\n $reportData['deal_value_max'] = (int) $data['max_deal_value'];\n\n if ($reportData['deal_value_max'] > 4294967295 || $reportData['deal_value_max'] < 0) {\n throw new InvalidArgumentException('Max deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['min_deal_value'], $data['max_deal_value'])\n && $data['min_deal_value'] > $data['max_deal_value']\n ) {\n throw new InvalidArgumentException('Min deal value cannot be greater than max deal value');\n }\n\n return $reportData;\n }\n\n private function validateDateRange(array $data, array $reportData, string $frequency): array\n {\n // Set date range only for one_off frequency\n if ($frequency === 'one_off') {\n if (isset($data['start_date_period'])) {\n $reportData['from'] = $this->parseDate($data['start_date_period']);\n }\n\n if (isset($data['end_date_period'])) {\n $reportData['to'] = $this->parseDate($data['end_date_period']);\n }\n\n if (empty($reportData['from']) || empty($reportData['to'])) {\n throw new InvalidArgumentException(\n 'Start date and end date are required for one_off frequency'\n );\n }\n } else {\n $reportData['from'] = null;\n $reportData['to'] = null;\n }\n\n return $reportData;\n }\n\n private function validateCallDurations(array $data, array $reportData): array\n {\n // Convert call durations from minutes to seconds\n if (isset($data['min_call_duration'])) {\n $reportData['call_duration_min'] = (int) $data['min_call_duration'] * 60;\n\n if ($reportData['call_duration_min'] > 4294967295 || $reportData['call_duration_min'] < 0) {\n throw new InvalidArgumentException('Min call duration should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_call_duration'])) {\n $reportData['call_duration_max'] = (int) $data['max_call_duration'] * 60;\n\n if ($reportData['call_duration_max'] > 4294967295 || $reportData['call_duration_max'] < 0) {\n throw new InvalidArgumentException('Max call duration should be between 0 and 4294967295');\n }\n }\n\n return $reportData;\n }\n\n private function validateCallTypes(array $data, array $reportData): array\n {\n // Set call types\n $reportData['call_types'] = $data['call_type'] ?? [];\n if (empty($reportData['call_types'])) {\n $reportData['call_types'] = self::getCallTypes();\n }\n\n foreach ($reportData['call_types'] as $callType) {\n if (! in_array($callType, self::getCallTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Call type %s is invalid', $callType));\n }\n }\n\n return $reportData;\n }\n\n private function validateMediaTypes(array $data, array $reportData): array\n {\n // Set media types from input data\n $reportData['media_types'] = $data['media_types'] ?? [];\n\n if (empty($reportData['media_types'])) {\n throw new InvalidArgumentException('Media types are required');\n }\n\n foreach ($reportData['media_types'] as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n throw new InvalidArgumentException(sprintf('Media type %s is invalid', $mediaType));\n }\n }\n\n return $reportData;\n }\n\n private function validateDealStages(array $data, array $reportData, Team $team, string $reportType): array\n {\n // Validate and set deal stages\n if (isset($data['deal_stage_at_call'])) {\n $reportData['deal_at_call_stages'] =\n $this->validateAndGetDealStageIds($team, $data['deal_stage_at_call'], 'Deal stage at call');\n }\n\n if (isset($data['current_deal_stage'])) {\n $reportData['current_deal_stages'] =\n $this->validateAndGetDealStageIds($team, $data['current_deal_stage'], 'Current deal stage');\n }\n\n // Ensure current_deal_stage is not provided for loss_analysis report type\n if ($reportType === self::TYPE_LOSS_ANALYSIS && ! empty($data['current_deal_stage'])) {\n throw new InvalidArgumentException('Current deal stage is not applicable for Loss Analysis reports');\n }\n\n return $reportData;\n }\n\n // transform uuid to id\n private function validatePlaybookCategories(array $data, array $reportData, Team $team): array\n {\n $key = 'playbook_categories';\n\n if (isset($data[$key])) {\n $payloadIds = $data[$key];\n $ids = [];\n\n foreach ($payloadIds as $uuid) {\n $uuid = (string) $uuid;\n\n try {\n $playbookCategory = $this->playbookCategoryRepository->findByUuid($uuid);\n } catch (Throwable $throwable) {\n Log::error(__METHOD__ . ' ' . $throwable->getMessage());\n\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory) {\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory->hasPlaybook()) {\n throw new InvalidArgumentException(sprintf('Playbook category %s has no playbook', $uuid));\n }\n\n if ($playbookCategory->getPlaybook()->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Playbook category %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $ids[] = $playbookCategory->getId();\n }\n\n $reportData[$key] = $ids;\n }\n\n return $reportData;\n }\n\n private function validateReportStatus($status): bool\n {\n if (! in_array($status, [true, false], true)) {\n throw new InvalidArgumentException('Report status is invalid');\n }\n\n return $status;\n }\n\n private function validateReportType($type): string\n {\n if (! in_array($type, self::getTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Report type is invalid: %s', $type));\n }\n\n return $type;\n }\n\n private function validateFrequency($frequency): string\n {\n if (! in_array($frequency, self::getFrequencies(), true)) {\n throw new InvalidArgumentException('Frequency is invalid');\n }\n\n return $frequency;\n }\n\n private function validateAdditionalPromptInput(?string $additionalPromptInput): ?string\n {\n if ($additionalPromptInput && strlen($additionalPromptInput) > 5000) {\n throw new InvalidArgumentException('Additional Prompt Input should be less than 5000 characters');\n }\n\n return $additionalPromptInput;\n }\n\n private function validateCustomReportName(?string $customReportName): ?string\n {\n if ($customReportName === null || $customReportName === '') {\n return null;\n }\n\n if (strlen($customReportName) > 70) {\n throw new InvalidArgumentException('Custom report name should be less than 70 characters');\n }\n\n return $customReportName;\n }\n\n private function validateOrganization(?string $organizationUuid): Team\n {\n if (! $organizationUuid) {\n throw new InvalidArgumentException('Organization is required');\n }\n\n $team = $this->teamRepository->idOrUuid($organizationUuid);\n\n if (! $team) {\n throw new InvalidArgumentException('Organization not found');\n }\n\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n throw new InvalidArgumentException('Organization does not have the Automated Reports feature');\n }\n\n return $team;\n }\n\n private function validateAndGetGroupIds(Team $team, array $teamUuids): array\n {\n $groupIds = [];\n\n foreach ($teamUuids as $uuid) {\n $group = $this->groupRepository->findByUuid($uuid);\n\n if ($group === null || $group->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Group %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $groupIds[] = $group->getId();\n\n }\n\n return $groupIds;\n }\n\n private function validateAndGetDealStageIds(Team $team, array $stageUuids, string $propertyLabel): array\n {\n $stageIds = [];\n\n foreach ($stageUuids as $uuid) {\n $stage = $this->stageRepository->findByUuid($uuid);\n\n if ($stage === null || $stage->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Stage %s not found for team %s for %s', $uuid, $team->getUuid(), $propertyLabel)\n );\n }\n\n $stageIds[] = $stage->getId();\n }\n\n return $stageIds;\n }\n\n private function validateAndGetUserIds(array $userUuids, callable $teamCheck): array\n {\n if (empty($userUuids)) {\n return [];\n }\n\n $userIds = [];\n\n foreach ($userUuids as $uuid) {\n $user = $this->userRepository->findByUuid($uuid);\n\n if (! $user || ! $user->isStatusActive()) {\n throw new InvalidArgumentException(\n sprintf('User %s not found or is not active', $uuid)\n );\n }\n\n if (! $teamCheck($user)) {\n throw new InvalidArgumentException(\n sprintf('User %s does not belong to the allowed team(s)', $uuid)\n );\n }\n\n $userIds[] = $user->getId();\n }\n\n return $userIds;\n }\n\n private function validateAndGetUserIdsByTeam(Team $team, array $userUuids): array\n {\n return $this->validateAndGetUserIds($userUuids, fn ($user) => $user->getTeamId() === $team->getId());\n }\n\n private function validateAndGetJiminnyUserIds(array $userUuids): array\n {\n $allowedTeamIds = config('kiosk.teamIds', []);\n\n return $this->validateAndGetUserIds($userUuids, fn ($user) => in_array($user->getTeamId(), $allowedTeamIds, true));\n }\n\n private function parseDate(string $dateString): string\n {\n return date('Y-m-d H:i:s', strtotime($dateString));\n }\n\n private function generateReportResultViewUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.view', ['uuid' => $result->getUuid()]);\n }\n\n private function generateReportResultDownloadUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.download', ['uuid' => $result->getUuid()]);\n }\n\n private function getReportMediaRouteResource(AutomatedReportResult $result): string\n {\n if ($result->getMediaType() === self::MEDIA_TYPE_PDF) {\n return self::PDF_KEY;\n } elseif ($result->getMediaType() === self::MEDIA_TYPE_PODCAST) {\n return self::AUDIO_KEY;\n }\n\n throw new \\InvalidArgumentException('Unknown media type.');\n }\n\n public function getMediaPath(AutomatedReportResult $result): ?string\n {\n $url = match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => $result->getPdfUrl(),\n self::MEDIA_TYPE_PODCAST => $result->getPodcastAudioUrl(),\n default => null,\n };\n\n if ($url === null) {\n return null;\n }\n\n $path = parse_url(trim($url, '\"\\''), PHP_URL_PATH);\n\n return $path ?: null;\n }\n\n public function getFilenameSuffix(AutomatedReportResult $result): ?string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => 'Podcast',\n default => null,\n };\n }\n\n public function getMailSubjectSuffix(AutomatedReportResult $result): string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => 'report',\n self::MEDIA_TYPE_PODCAST => 'podcast',\n default => '',\n };\n }\n\n public function getMediaTypeMetadata(AutomatedReportResult $result): array\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => ['extension' => 'mp3', 'mime' => 'audio/mpeg'],\n self::MEDIA_TYPE_PDF => ['extension' => 'pdf', 'mime' => 'application/pdf'],\n default => ['extension' => null, 'mime' => null],\n };\n }\n\n public function deleteS3Files(AutomatedReportResult $result): void\n {\n $teamUuid = $result->getReport()->getTeam()->getUuid();\n $reportUuid = $result->getUuid();\n\n // delete all files for a report uuid no mather of pdf, podcast, or both\n // in case of both - the podcast files are linked to the pdf (parent) uuid\n // pdf and podcast date times should be close\n $path = sprintf('%s/%s/%s', $teamUuid, self::S3_DIR, $reportUuid);\n\n foreach (self::FILE_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted S3 file', [\n 'path' => $file,\n ]);\n }\n }\n\n foreach (self::FILE_PODCAST_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '_podcast.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted Podcast S3 file', [\n 'path' => $file,\n ]);\n }\n }\n }\n\n /**\n *\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): Collection\n {\n return $this->automatedReportsRepository->getTeamIdsWithReportsResults($teamId);\n }\n\n /**\n * Core delete logic for report results using a query\n *\n * @param Builder $query\n * @param array $logContext\n *\n * @return int\n */\n private function deleteReportResultsByQuery(Builder $query, array $logContext = []): int\n {\n $deletedCount = 0;\n\n if ($query->exists()) {\n Log::info(\n 'Run delete report results',\n array_merge(\n $logContext,\n [\n 'service' => 'AutomatedReportsService',\n ]\n )\n );\n\n $query->chunkById(50, function ($results) use (&$deletedCount, $logContext) {\n foreach ($results as $result) {\n $this->deleteReportResult($result);\n $deletedCount++;\n\n Log::info(\n 'Deleted a report result',\n array_merge(\n $logContext,\n [\n 'result_id' => $result->getId(),\n 'report_id' => $result->getReportId(),\n ]\n )\n );\n }\n });\n }\n\n return $deletedCount;\n }\n\n /**\n * Delete report results for a team by retention period\n *\n * @param Team $team\n * @param CarbonImmutable $retentionDate\n *\n * @return int Number of deleted report results\n */\n public function deleteReportsResultsInRetentionPeriod(Team $team, CarbonImmutable $retentionDate): int\n {\n $reportIds = $this->automatedReportsRepository->getReportIdsByTeam($team);\n\n if ($reportIds->isEmpty()) {\n return 0;\n }\n\n $query = $this->automatedReportsRepository\n ->getReportResultsQueryForRetention($team, $retentionDate);\n\n return $this->deleteReportResultsByQuery($query, [\n 'team_id' => $team->getId(),\n 'retention_date' => $retentionDate->toDateTimeString(),\n ]);\n }\n\n /**\n * Delete ALL report results for a specific automated report\n *\n * @param string $uuid\n *\n * @return int\n */\n public function deleteReportResults(string $uuid): int\n {\n $report = $this->getReport($uuid);\n\n $query = $this->automatedReportsRepository->getResultsByReportQuery($report);\n\n return $this->deleteReportResultsByQuery($query, [\n 'report_uuid' => $uuid,\n 'report_id' => $report->getId(),\n ]);\n }\n\n public function deleteReportResult(AutomatedReportResult $result): void\n {\n $this->deleteS3Files($result);\n\n $result->delete();\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getTeamReports(Team $team): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getReportsByTeam($team);\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getReportResults(AutomatedReport $report): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getResultsByReport($report);\n }\n\n public function deleteAllReportResults(AutomatedReport $report): void\n {\n $results = $this->getReportResults($report);\n\n /** @var AutomatedReportResult $result */\n foreach ($results as $result) {\n Log::info('Deleting result', [\n 'report' => $report->getId(),\n 'result' => $result->getId(),\n ]);\n\n $this->deleteReportResult($result);\n }\n }\n\n public function deleteAllData(Team $team): void\n {\n Log::info('Deleting automated report and results for team', [\n 'team' => $team->getId(),\n ]);\n\n $reports = $this->getTeamReports($team);\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n Log::info('Deleting report', [\n 'team' => $team->getId(),\n 'report' => $report->getId(),\n ]);\n\n $this->deleteAllReportResults($report);\n\n $report->delete();\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Carbon\\CarbonImmutable;\nuse Carbon\\CarbonInterface;\nuse Carbon\\Exceptions\\InvalidFormatException;\nuse DateTime;\nuse DateTimeInterface;\nuse DateTimeZone;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\InputTypeEnum;\nuse Jiminny\\Component\\AskAnything\\AskAnythingPromptService;\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Contracts\\Repositories\\PlaybookCategoryRepository;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\ModelNotFoundException;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Partner;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Repositories\\GroupRepository;\nuse Jiminny\\Repositories\\SearchRepository;\nuse Jiminny\\Repositories\\StageRepository;\nuse Throwable;\n\nclass AutomatedReportsService\n{\n public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';\n public const string TYPE_ASK_JIMINNY = 'ask_jiminny';\n\n /**\n * Standard report types (used by kiosk for existing automated reports).\n */\n // @TODO this will add filter, however if we need to control feature by FF we need conditional logic\n public const array TYPES = [\n ['id' => 'exec_summary', 'name' => 'Exec Summary'],\n ['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],\n ['id' => 'product_feedback', 'name' => 'Product Feedback'],\n ['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],\n// ['id' => 'questions', 'name' => 'Questions'],\n// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],\n ];\n\n public const array ALL_TYPES = [\n ...self::TYPES,\n ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],\n ];\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\n /**\n * Frequencies for standard (non-Ask Jiminny) reports.\n */\n public const array FREQUENCIES = [\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n\n /**\n * Frequencies for Ask Jiminny reports.\n */\n public const array ASK_JIMINNY_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ];\n\n public const string MEDIA_TYPE_PDF = 'pdf';\n public const string MEDIA_TYPE_PODCAST = 'podcast';\n public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];\n public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];\n public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];\n public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];\n\n public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];\n public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];\n public const int SENT_REPORT_AT_HOURS = 5;\n public const string PDF_KEY = 'pdf';\n public const string AUDIO_KEY = 'audio';\n\n private const array ALL_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n private const string S3_DIR = 'reports';\n private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];\n private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];\n\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly GroupRepository $groupRepository,\n private readonly UserRepository $userRepository,\n private readonly StageRepository $stageRepository,\n private readonly DealStagesService $dealStagesService,\n private readonly RecipientsService $recipientsService,\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly Webhook $webhookService,\n private readonly BusDispatcher $dispatcher,\n private readonly ActivityTypeService $activityTypeService,\n private readonly PlaybookCategoryRepository $playbookCategoryRepository,\n private readonly AskAnythingPromptService $askAnythingPromptService,\n private readonly SearchRepository $activitySearchRepository,\n private readonly AskAnythingRepository $askAnythingRepository,\n ) {\n }\n\n public static function getTypes(): array\n {\n $types = self::TYPES;\n\n return array_map(static function ($type) {\n return $type['id'];\n }, $types);\n }\n\n public static function getCallTypes(): array\n {\n return array_map(static function ($callType) {\n return $callType['id'];\n }, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);\n }\n\n public static function getFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::FREQUENCIES);\n }\n\n // front-facing structure\n public function getReportEnabledFieldData(bool $value = false): array\n {\n return [\n 'id' => 'report_enabled',\n 'label' => '',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'value' => $value,\n ];\n }\n\n // Organizations = Teams\n public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array\n {\n $options = $this->getTeams(partner: $partner);\n\n if ($shortVersion) {\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value,\n 'dependencies' => [\n 'teams',\n 'deal_stage_at_call',\n 'current_deal_stage',\n 'recipients',\n ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,\n ],\n 'dependsOn' => [],\n ];\n }\n\n // Teams = Groups\n public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array\n {\n if ($shortVersion) {\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value, // value should be an array of objects {id, name}\n 'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],\n 'dependsOn' => [],\n ];\n }\n\n public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array\n {\n $types = [];\n if ($team instanceof Team) {\n if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n $types = self::TYPES;\n }\n if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {\n $types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];\n }\n } else {\n $types = self::TYPES;\n }\n\n if ($shortVersion) {\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'options' => $types,\n ];\n }\n\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $types,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getFrequencyFieldData(?string $value = null): array\n {\n return [\n 'id' => 'frequency',\n 'label' => 'Frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::FREQUENCIES,\n 'value' => $value,\n 'dependencies' => ['period'],\n 'dependsOn' => [],\n ];\n }\n\n public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array\n {\n return [\n 'id' => 'period',\n 'label' => 'Select one-off period',\n 'inputType' => InputTypeEnum::DATE_RANGE,\n 'required' => true,\n 'placeholder' => 'Select',\n 'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],\n 'queryParams' => [\n 'startDate' => 'start_date_period',\n 'endDate' => 'end_date_period',\n ],\n 'dependencies' => [],\n 'dependsOn' => ['frequency'],\n ];\n }\n\n public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array\n {\n return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);\n }\n\n public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);\n }\n\n public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);\n }\n\n public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'deal_value',\n 'label' => 'Deal Value',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_deal_value',\n 'max' => 'max_deal_value',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array\n {\n $value = [];\n $conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;\n $dialerOn && $value[] = self::CALL_TYPE_DIALER;\n\n return [\n 'id' => 'call_type',\n 'label' => 'Call Type',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => [\n self::CALL_TYPE_CONFERENCE,\n self::CALL_TYPE_DIALER,\n ],\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getMediaTypeFieldData(?AutomatedReport $report = null): array\n {\n $value = [];\n\n if ($report) {\n $value = $this->transformMediaTypes($report);\n }\n\n return [\n 'id' => 'media_types',\n 'label' => 'Export as',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => self::MEDIA_TYPE_OBJECTS,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'call_duration',\n 'label' => 'Call Duration',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_call_duration',\n 'max' => 'max_call_duration',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getRecipientsFieldData(?Team $team = null, array $value = []): array\n {\n return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);\n }\n\n public function getJiminnyRecipientsFieldData(array $value = []): array\n {\n return $this->recipientsService->getJiminnyRecipientsFieldData($value);\n }\n\n public function getAdditionalPromptInputFieldData(?string $value = null): array\n {\n return [\n 'id' => 'additional_prompt_input',\n 'label' => 'Special requirements',\n 'inputType' => InputTypeEnum::TEXTAREA,\n 'required' => false,\n 'placeholder' => 'What should be the focus of the report?',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCustomReportNameFieldData(?string $value = null): array\n {\n return [\n 'id' => 'custom_name',\n 'label' => 'Custom report name',\n 'inputType' => InputTypeEnum::TEXT,\n 'required' => false,\n 'placeholder' => 'Enter custom name',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n // data providers\n public function getTeams(?Partner $partner = null): array\n {\n $teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);\n\n $teamData = [];\n foreach ($teams as $team) {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n continue;\n }\n\n $teamData[] = $this->transformTeam($team);\n }\n\n return $teamData;\n }\n\n public function getTeamGroups(string $teamUuid): array\n {\n $data = [];\n $team = $this->getTeam($teamUuid);\n\n if ($team !== null) {\n $groups = $team->groups()->get();\n\n foreach ($groups as $group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array\n {\n $data = [];\n $teams = $this->getTeams(partner: $partner);\n\n foreach ($teams as $team) {\n if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {\n continue;\n }\n\n $data[] = [\n 'label' => $team['name'],\n 'groups' => $this->getTeamGroups($team['id']),\n ];\n }\n\n return $data;\n }\n\n public function getTeam(string $teamUuid): ?Team\n {\n return $this->teamRepository->idOrUuid($teamUuid);\n }\n\n public function getTeamById(int $teamId): ?Team\n {\n return $this->teamRepository->find($teamId);\n }\n\n public function getGroupsUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportGroups = $report->getGroups();\n foreach ($reportGroups as $groupId) {\n if ($group = $this->groupRepository->find($groupId)) {\n $uuids[] = $group->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getPlaybookCategoriesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $playbookCategories = $report->getPlaybookCategories();\n foreach ($playbookCategories as $id) {\n if ($category = $this->playbookCategoryRepository->find($id)) {\n $uuids[] = $category->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getDealAtCallStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getDealAtCallStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getCurrentDealStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getCurrentDealStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getRecipients());\n }\n\n public function getJiminnyUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function extractUserUuids(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => $user->getUuid())\n ->values()\n ->all();\n }\n\n // get mail data\n public function getRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getRecipients());\n }\n\n /**\n * @return array<UserContract>\n */\n public function getRecipientUserObjects(AutomatedReport $report): array\n {\n $userIds = $report->getRecipients()['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->values()\n ->all();\n }\n\n private function getJiminnyRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function buildRecipientUsers(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => [\n 'email' => $user->getEmailAddress(),\n 'name' => $user->getName(),\n 'timezone' => $user->getTimezone()->getName(),\n ])\n ->values()\n ->all();\n }\n\n public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array\n {\n if ($report->isAskJiminnyReport()) {\n $recipients = $this->resolveAskJiminnyRecipients($report);\n } else {\n $recipients = $this->getRecipientUsers($report);\n if ($includeJiminny) {\n $recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));\n }\n }\n\n $emails = [];\n\n return array_values(array_filter(\n $recipients,\n static function ($recipient) use (&$emails) {\n if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {\n return false;\n }\n $emails[] = $recipient['email'];\n\n return true;\n }\n ));\n }\n\n private function resolveAskJiminnyRecipients(AutomatedReport $report): array\n {\n $recipients = [];\n\n $creator = $report->getCreator();\n if ($creator !== null) {\n $recipients[] = [\n 'email' => $creator->getEmailAddress(),\n 'name' => $creator->getName(),\n 'timezone' => $creator->getTimezone()->getName(),\n ];\n }\n\n return array_merge(\n $recipients,\n $this->buildRecipientUsers($report->getRecipients()),\n $this->getGroupRecipientUsers($report),\n );\n }\n\n private function getGroupRecipientUsers(AutomatedReport $report): array\n {\n $users = [];\n foreach ($report->getGroups() as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group === null) {\n continue;\n }\n foreach ($group->getMembers() as $member) {\n $users[] = [\n 'email' => $member->getEmailAddress(),\n 'name' => $member->getName(),\n 'timezone' => $member->getTimezone()->getName(),\n ];\n }\n }\n\n return $users;\n }\n\n public function getReportTypeName(AutomatedReportResult $report): string\n {\n $type = $report->getReport()->getType();\n\n $getType = $this->transformReportType($type);\n\n return $getType['name'];\n }\n\n public function getReportPeriodName(AutomatedReportResult $report): string\n {\n $from = $report->getFromDate();\n $to = $report->getToDate();\n $frequency = $report->getReport()->getFrequency();\n\n if ($from === null || $to === null) {\n if (! $report->getReport()->isAskJiminnyReport()) {\n $invalidPeriod = $from === null ? 'from' : 'to';\n\n throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);\n }\n\n $timezone = $report->getReport()->getCreator()?->getTimezone();\n $period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);\n $from = $period['fromDate'];\n $to = $period['toDate'];\n }\n\n return $this->formatReportPeriodName($frequency, $from, $to);\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 getReportTeamsName(AutomatedReportResult $report): string\n {\n $groups = $report->getGroups();\n\n if (empty($groups)) {\n return 'All';\n }\n\n // Get group names from repository\n $groupNames = [];\n foreach ($groups as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group) {\n $groupNames[] = $group->getName();\n }\n }\n\n if (count($groupNames) === 1) {\n // Single team format\n $teamsName = $groupNames[0];\n } else {\n // Multiple teams format\n $teamsName = implode(', ', $groupNames);\n }\n\n return $teamsName;\n }\n\n public function getReportFileName(AutomatedReportResult $report): string\n {\n $customName = $report->getReport()->getCustomName();\n $periodName = $this->getReportPeriodName($report);\n $filenameSuffix = $this->getFilenameSuffix($report);\n\n if ($customName) {\n if ($filenameSuffix) {\n $customName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$customName} - {$periodName}\");\n }\n\n $baseName = $this->getReportTypeName($report);\n\n if ($filenameSuffix) {\n $baseName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}\");\n }\n\n public function getReportFileNameWithExtension(AutomatedReportResult $result): string\n {\n $extension = $this->getMediaTypeMetadata($result)['extension'];\n\n return $this->getReportFileName($result) . '.' . $extension;\n }\n\n public function sanitizeFileName(string $fileName): string\n {\n return str_replace(['/', '\\\\'], '-', $fileName);\n }\n\n public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool\n {\n $recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);\n if (in_array($user->getId(), $recipientIds, true)) {\n return true;\n }\n\n if ($report->isAskJiminnyReport()) {\n $groupId = $user->getGroupId();\n if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {\n return true;\n }\n }\n\n return false;\n }\n\n public function transformReportResults(Collection $automatedReportResults): array\n {\n $data = [];\n foreach ($automatedReportResults as $automatedReportResult) {\n /** @var AutomatedReportResult $automatedReportResult */\n\n $report = $automatedReportResult->getReport();\n\n $createdBy = $report->getCreator();\n $creator = [\n 'id' => $createdBy?->getUuid(),\n 'name' => $createdBy?->getName(),\n 'email' => $createdBy?->getEmailAddress(),\n 'photoUrl' => $createdBy?->getPhotoUrl(),\n ];\n\n $data[] = [\n 'id' => $automatedReportResult->getUuid(),\n 'name' => $automatedReportResult->getName(),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n 'recipients' => $this->buildRecipients($report),\n 'report_type' => $this->transformReportType($report->getType()),\n 'media_type' => $automatedReportResult->getMediaType(),\n 'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),\n 'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),\n 'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),\n 'creator' => $creator,\n ];\n }\n\n return $data;\n }\n\n private function buildRecipients(AutomatedReport $report): array\n {\n $creatorUuid = $report->getCreator()?->getUuid();\n\n $recipients = array_values(array_filter(\n $this->transformRecipients($report->getRecipients()),\n static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,\n ));\n\n if (! $report->isAskJiminnyReport()) {\n return $recipients;\n }\n\n return [\n ...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),\n ...$recipients,\n ];\n }\n\n public function hasCallTypeConference(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);\n }\n\n public function hasCallTypeDialer(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);\n }\n\n // transformers\n private function transformTeam(Team $team): array\n {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n return [];\n }\n\n return [\n 'id' => $team->getUuid(),\n 'name' => $team->getName(),\n ];\n }\n\n private function transformReportFullView(AutomatedReport $report): array\n {\n $base = $this->transformReportBase($report);\n\n return $report->getType() === self::TYPE_ASK_JIMINNY\n ? $base + $this->transformAskJiminnyFields($report)\n : $base + $this->transformStandardReportFields($report);\n }\n\n private function transformReportBase(AutomatedReport $report): array\n {\n return [\n 'id' => $report->getUuid(),\n 'organization' => $this->transformOrganization(team: $report->getTeam()),\n 'report_type' => $this->transformReportType($report->getType()),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n ];\n }\n\n private function transformStandardReportFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n\n return [\n 'report_enabled' => $report->getStatus(),\n 'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),\n 'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),\n 'deal_value_min' => $report->getDealValueMin(),\n 'deal_value_max' => $report->getDealValueMax(),\n 'call_types' => $this->transformCallType($report->getCallTypes()),\n 'media_types' => $this->transformMediaTypes($report),\n 'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),\n 'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),\n 'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),\n 'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),\n 'recipients' => $this->transformRecipients($report->getRecipients()),\n 'created_by' => $this->transformCreator($report->getCreator()),\n 'additional_prompt_input' => $report->getAdditionalPromptInput(),\n 'custom_name' => $report->getCustomName(),\n 'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),\n 'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),\n 'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),\n ];\n }\n\n private function transformAskJiminnyFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n $creatorId = $report->getAttribute('created_by');\n $explicitUserIds = array_values(array_filter(\n $report->getRecipients()['users'] ?? [],\n static fn ($id) => $id !== $creatorId\n ));\n\n return [\n 'report_name' => $report->getCustomName(),\n 'enabled' => $report->getStatus(),\n 'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),\n 'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),\n 'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),\n 'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),\n ];\n }\n\n private function transformOrganization(?Team $team): array\n {\n return [\n 'id' => $team?->getUuid(),\n 'name' => $team?->getName(),\n ];\n }\n\n private function transformReportType(string $type): array\n {\n foreach (self::ALL_TYPES as $typeItem) {\n if ($typeItem['id'] === $type) {\n return $typeItem;\n }\n }\n\n return [];\n }\n\n private function transformCallType(array $types): array\n {\n $result = [];\n $callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];\n\n foreach ($types as $type) {\n foreach ($callTypes as $callTypeItem) {\n if ($callTypeItem['id'] === $type) {\n $result[] = $callTypeItem;\n\n break;\n }\n }\n }\n\n return $result;\n }\n\n private function transformMediaTypes(AutomatedReport $report): array\n {\n $values = [];\n\n foreach ($report->getMediaTypes() as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n continue;\n }\n\n $values[] = match ($mediaType) {\n self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,\n self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,\n };\n }\n\n return $values;\n }\n\n private function transformFrequency(string $frequency): array\n {\n foreach (self::ALL_FREQUENCIES as $frequencyItem) {\n if ($frequencyItem['id'] === $frequency) {\n return $frequencyItem;\n }\n }\n\n return [];\n }\n\n public function transformDurationToMinutes(?int $duration): ?int\n {\n if (! $duration) {\n return null;\n }\n\n return (int) ($duration / 60);\n }\n\n private function transformGroups(?Team $team, array $groupsIds): array\n {\n if (empty($groupsIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($groupsIds as $groupId) {\n $group = $team->groups()->where('id', $groupId)->first();\n\n if ($group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n 'photoUrl' => $group->getPhotoUrl(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformStages(?Team $team, array $stagesIds): array\n {\n if (empty($stagesIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($stagesIds as $stageId) {\n $stage = $team->stages()->where('id', $stageId)->first();\n\n if ($stage) {\n $data[] = [\n 'id' => $stage->getUuid(),\n 'name' => $stage->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformRecipients(array $recipients): array\n {\n $users = [];\n foreach ($recipients['users'] ?? [] as $userId) {\n $users[] = $this->transformUser($userId);\n }\n\n return $users;\n }\n\n private function transformCreator(?User $user): ?array\n {\n if ($user === null) {\n return null;\n }\n\n return $this->transformUser($user->getId());\n }\n\n private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array\n {\n if ($prompt === null) {\n return null;\n }\n\n return [\n 'id' => $prompt->getUuid(),\n 'name' => $prompt->getTitle(),\n ];\n }\n\n private function transformSafeSearch(?Search $search): ?array\n {\n if ($search === null) {\n return null;\n }\n\n return [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ];\n }\n\n private function transformUser(int $userId): array\n {\n /* @var ?User $user */\n $user = $this->userRepository->find($userId);\n\n return [\n 'id' => $user?->getUuid(),\n 'name' => $user?->getName(),\n 'email' => $user?->getEmailAddress(),\n 'photoUrl' => $user?->getPhotoUrl(),\n ];\n }\n\n public function create(array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $validatedData['created_by'] = auth()->id();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function update(string $uuid, array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $report = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $report) {\n throw new InvalidArgumentException('Report not found');\n }\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Create an Ask Jiminny report.\n */\n public function createAskJiminnyReport(array $data, User $creator): array\n {\n $validatedData = $this->validateAskJiminnyReportData($data, $creator);\n $validatedData['created_by'] = $creator->getId();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Update an Ask Jiminny report.\n */\n public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array\n {\n if (! $report->isAskJiminnyReport()) {\n throw new InvalidArgumentException('Report is not an Ask Jiminny report');\n }\n\n $validatedData = $this->validateAskJiminnyReportData($data, $user);\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array\n {\n if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {\n throw new InvalidArgumentException(\n 'This report is missing a saved search or prompt. ' .\n 'Edit the report to complete the setup before enabling it.'\n );\n }\n\n $this->automatedReportsRepository->update($report, ['status' => $status]);\n\n return $this->transformReportFullView($report->fresh());\n }\n\n /**\n * Validate and transform data for Ask Jiminny reports.\n */\n private function validateAskJiminnyReportData(array $data, User $user): array\n {\n // Validate name\n $name = trim($data['report_name'] ?? '');\n if (empty($name)) {\n throw new InvalidArgumentException('Report name is required');\n }\n if (mb_strlen($name) > 50) {\n throw new InvalidArgumentException('Report name must be 50 characters or less');\n }\n\n // Validate frequency (only daily, weekly, monthly for Ask Jiminny)\n $frequency = $data['frequency'] ?? null;\n $askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];\n if (! in_array($frequency, $askJiminnyFrequencies, true)) {\n throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');\n }\n\n // Validate expiration date\n $expiresAt = $data['expires_on'] ?? null;\n if (empty($expiresAt)) {\n throw new InvalidArgumentException('Expiration date is required');\n }\n\n try {\n $expiresAtDate = Carbon::parse($expiresAt);\n } catch (InvalidFormatException $e) {\n throw new InvalidArgumentException('Expiration date format is invalid');\n }\n $maxExpiration = Carbon::now()->addYear()->endOfDay();\n if ($expiresAtDate->gt($maxExpiration)) {\n throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');\n }\n if ($expiresAtDate->isPast()) {\n throw new InvalidArgumentException('Expiration date cannot be in the past');\n }\n\n // Validate saved search\n $activitySearchId = $data['saved_search'] ?? null;\n if (empty($activitySearchId)) {\n throw new InvalidArgumentException('Saved search is required');\n }\n $savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);\n if (! $savedSearch) {\n throw new InvalidArgumentException('Saved search not found or does not belong to you');\n }\n\n // Validate saved prompt\n $askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;\n if (empty($askAnythingPromptId)) {\n throw new InvalidArgumentException('Ask Jiminny prompt is required');\n }\n $prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);\n if (! $prompt) {\n throw new InvalidArgumentException('Ask Jiminny prompt not found');\n }\n\n // Validate status\n $status = $data['enabled'] ?? false;\n\n $recipientUserIds = [$user->getId()];\n\n if (! empty($data['share_users'])) {\n $sharedUserIds = $this->validateAndGetUserIdsByTeam(\n $user->team,\n (array) $data['share_users']\n );\n $recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);\n }\n\n $sharedGroupIds = [];\n if (! empty($data['share_teams'])) {\n $sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);\n }\n\n $recipientUserIds = array_values(array_unique($recipientUserIds));\n\n return [\n 'team_id' => $user->getTeamId(),\n 'type' => self::TYPE_ASK_JIMINNY,\n 'status' => (bool) $status,\n 'frequency' => $frequency,\n 'custom_name' => $name,\n 'activity_search_id' => $savedSearch->getId(),\n 'ask_anything_prompt_id' => $prompt->getId(),\n 'expires_at' => $expiresAtDate->toDateString(),\n 'media_types' => [self::MEDIA_TYPE_PDF],\n 'call_types' => [],\n 'recipients' => ['users' => $recipientUserIds],\n 'groups' => $sharedGroupIds,\n ];\n }\n\n public static function getAskJiminnyFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::ASK_JIMINNY_FREQUENCIES);\n }\n\n public function getAskJiminnyReportFilters(User $user): array\n {\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n return [\n [\n 'id' => 'prompt',\n 'label' => 'Prompt',\n 'options' => $prompts,\n ],\n [\n 'id' => 'saved_search',\n 'label' => 'Saved Search',\n 'options' => $savedSearches,\n ],\n ];\n }\n\n public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array\n {\n $team = $user->getTeam();\n $userTimezone = $user->getTimezone();\n\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n $teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ])->values()->all();\n\n $shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];\n\n $sharedTeamsValue = [];\n $sharedUsersValue = [];\n if ($report) {\n $sharedTeamsValue = $this->transformGroups($team, $report->getGroups());\n\n $recipientUserIds = $report->getRecipients()['users'] ?? [];\n $creatorId = $report->getAttribute('created_by');\n $sharedUserIds = array_values(array_filter(\n $recipientUserIds,\n static fn ($id) => $id !== $creatorId\n ));\n $sharedUsersValue = collect($sharedUserIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (User $u) => [\n 'id' => $u->getUuid(),\n 'name' => $u->getName(),\n ])\n ->values()\n ->all();\n }\n\n return [\n 'fields' => [\n [\n 'id' => 'enabled',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'label' => '',\n 'value' => $report?->getStatus() ?? false,\n ],\n [\n 'id' => 'report_name',\n 'inputType' => InputTypeEnum::TEXT,\n 'label' => 'Name',\n 'placeholder' => 'Enter name',\n 'required' => true,\n 'validation' => ['maxLength' => 50],\n 'value' => $report?->getCustomName() ?? '',\n ],\n [\n 'id' => 'frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Frequency',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::ASK_JIMINNY_FREQUENCIES,\n 'value' => $report ? $this->transformFrequency($report->getFrequency()) : null,\n ],\n [\n 'id' => 'expires_on',\n 'inputType' => InputTypeEnum::DATE,\n 'label' => 'Expires on',\n 'required' => true,\n 'placeholder' => 'Select',\n 'validation' => [\n 'minDate' => now($userTimezone)->toDateString(),\n 'maxDate' => now($userTimezone)->addYear()->toDateString(),\n ],\n 'value' => $report?->getExpiresAt()?->toDateString(),\n ],\n [\n 'id' => 'share_teams',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team',\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $teamGroups,\n 'value' => $sharedTeamsValue,\n ],\n [\n 'id' => 'share_users',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team member',\n 'required' => false,\n 'placeholder' => 'Select',\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'options' => $shareUsers,\n 'value' => $sharedUsersValue,\n ],\n [\n 'id' => 'saved_search',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Saved search',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $savedSearches,\n 'value' => $report && $report->getSavedSearch() ? [\n 'id' => $report->getSavedSearch()->getUuid(),\n 'name' => $report->getSavedSearch()->getName(),\n ] : null,\n ],\n [\n 'id' => 'ask_jiminny_prompt',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Ask Jiminny prompt',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $prompts,\n 'value' => $report && $report->getAskAnythingPrompt() ? [\n 'id' => $report->getAskAnythingPrompt()->getUuid(),\n 'name' => $report->getAskAnythingPrompt()->getTitle(),\n ] : null,\n ],\n ],\n ];\n }\n\n private function updateResultNames(AutomatedReport $automatedReport): void\n {\n $results = $this->automatedReportsRepository->getResultsByReport($automatedReport);\n\n foreach ($results as $result) {\n $result->update(['name' => $this->getReportFileName($result)]);\n }\n }\n\n public function updateStatus(string $uuid, array $data): array\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $automatedReport->update([\n 'status' => $status,\n ]);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n private function generateOneOffReport(AutomatedReport $automatedReport): void\n {\n // the scheduler handles all the other frequency types\n if ($automatedReport->getStatus() === false || $automatedReport->getFrequency() !== self::FREQUENCY_ONE_OFF) {\n return;\n }\n\n $this->dispatcher->dispatch(new RequestGenerateReportJob($automatedReport->getUuid()));\n }\n\n public function getReport(string $uuid, ?Partner $partner = null): AutomatedReport\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n if ($partner !== null && ! $partner->isDefaultPartner() && $automatedReport->team->partner_id !== $partner->getId()) {\n throw new ModelNotFoundException('Report not found');\n }\n\n return $automatedReport;\n }\n\n public function get(string $uuid, ?Partner $partner = null): array\n {\n $automatedReport = $this->getReport($uuid, $partner);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function list(string $sortColumn = 'created_at', string $sortDirection = 'desc', ?Partner $partner = null): array\n {\n $results = [];\n $collection = $this->automatedReportsRepository->getAllStandardReports($sortColumn, $sortDirection, $partner);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function listAskJiminnyReports(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): array {\n $results = [];\n $collection = $this->automatedReportsRepository->getAskJiminnyReportsByUser($user, $sortColumn, $sortDirection);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function delete(string $uuid): void\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $automatedReport->delete();\n }\n\n public function createReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n return $this->automatedReportsRepository->createResult(\n array_merge(\n [\n 'report_id' => $automatedReport->getId(),\n 'status' => AutomatedReportResult::STATUS_DEFAULT,\n ],\n $data\n )\n );\n }\n\n public function getOrCreateReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n $existing = $this->automatedReportsRepository->findLatestSameDayDefaultOrFailedResult($automatedReport);\n\n if ($existing !== null) {\n $existing->update(['status' => AutomatedReportResult::STATUS_DEFAULT]);\n\n return $existing;\n }\n\n return $this->createReportResult($automatedReport, $data);\n }\n\n public function getReportResult(string $resultUuid): AutomatedReportResult\n {\n $report = $this->automatedReportsRepository->findResultByUuid($resultUuid);\n\n if (! $report) {\n throw new ModelNotFoundException('Report Result not found');\n }\n\n return $report;\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return $this->automatedReportsRepository->findChildResult($result, $type);\n }\n\n // prophet API calls\n /**\n * @throws ApplicationException\n */\n public function getGenerateReportPayload(AutomatedReport $automatedReport, string $reportResultUuid): array\n {\n $period = $this->calculateFromAndToDate($automatedReport);\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n return [\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResultUuid,\n 'report_type' => $automatedReport->getType(),\n 'media_types' => $automatedReport->getMediaTypes(),\n 'from_date' => $fromDate->startOfDay()->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->endOfDay()->format(DateTimeInterface::RFC3339),\n 'group_ids' => $automatedReport->getGroups(),\n 'call_deal_stage' => $automatedReport->getDealAtCallStages(),\n 'current_deal_stage' => $automatedReport->getCurrentDealStages(),\n 'deal_min_value' => $automatedReport->getDealValueMin(),\n 'deal_max_value' => $automatedReport->getDealValueMax(),\n 'call_types' => $automatedReport->getCallTypes(),\n 'call_duration_min_seconds' => $automatedReport->getCallDurationMin(),\n 'call_duration_max_seconds' => $automatedReport->getCallDurationMax(),\n 'special_requirements' => $automatedReport->getAdditionalPromptInput(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->formatReportPeriodName(\n $automatedReport->getFrequency(),\n $fromDate,\n $toDate,\n ),\n 'playbook_categories' => $automatedReport->getPlaybookCategories(),\n 'custom_name' => $automatedReport->getCustomName(),\n ];\n }\n\n // $inputPayload - FE payload structure\n public function getActivitiesCountPayload(array $inputPayload): array\n {\n // Use validateAndTransformData to validate and normalize input\n $validatedData = $this->validateAndTransformData($inputPayload);\n $period = $this->calculateFromAndToDatePeriod(\n $validatedData['frequency'],\n Carbon::parse($validatedData['from']),\n Carbon::parse($validatedData['to']),\n );\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n // Create payload similar to getGenerateReportPayload\n return [\n 'team_id' => $validatedData['team_id'],\n 'group_ids' => $validatedData['groups'] ?? [],\n 'report_type' => $validatedData['type'],\n 'from_date' => $fromDate->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->format(DateTimeInterface::RFC3339),\n 'call_deal_stage' => $validatedData['deal_at_call_stages'] ?? [],\n 'current_deal_stage' => $validatedData['current_deal_stages'] ?? [],\n 'deal_min_value' => $validatedData['deal_value_min'] ?? null,\n 'deal_max_value' => $validatedData['deal_value_max'] ?? null,\n 'call_types' => $validatedData['call_types'],\n 'call_duration_min_seconds' => $validatedData['call_duration_min'] ?? null,\n 'call_duration_max_seconds' => $validatedData['call_duration_max'] ?? null,\n 'special_requirements' => $validatedData['additional_prompt_input'] ?? null,\n 'playbook_categories' => $validatedData['playbook_categories'] ?? [],\n 'request_id' => null,\n 'callback_url' => null,\n ];\n }\n\n public function shouldSendReport(array $users, ?CarbonInterface $generatedAt = null): bool\n {\n if (empty($users)) {\n return false;\n }\n\n $earliestTz = collect($users)\n ->mapWithKeys(function (array $user) {\n $tz = new DateTimeZone($user['timezone']);\n $nowUtc = new DateTime('now', new DateTimeZone('UTC'));\n $offset = $tz->getOffset($nowUtc);\n\n return [$user['timezone'] => $offset];\n })\n ->sortDesc()\n ->keys()\n ->first();\n\n $now = Carbon::now($earliestTz);\n $isScheduledTime = (int) $now->format('H') === self::SENT_REPORT_AT_HOURS;\n\n if ($isScheduledTime) {\n return true;\n }\n\n return $this->hasPassedScheduledTime($generatedAt, $earliestTz);\n }\n\n public function hasPassedScheduledTime(?CarbonInterface $generatedAt, string $timezone): bool\n {\n if ($generatedAt === null) {\n return false;\n }\n\n $now = Carbon::now($timezone);\n $scheduledTime = $now->copy()->setTime(self::SENT_REPORT_AT_HOURS, 0, 0);\n\n if ($now->hour < self::SENT_REPORT_AT_HOURS) {\n $scheduledTime = $scheduledTime->subDay();\n }\n\n $scheduledTimeUtc = $scheduledTime->copy()->utc();\n $generatedAtUtc = $generatedAt->copy()->utc();\n $nowUtc = $now->copy()->utc();\n\n return $generatedAtUtc->lt($scheduledTimeUtc) && $nowUtc->gt($scheduledTimeUtc);\n }\n\n public function calculateFromAndToDatePeriod(\n string $frequency,\n ?Carbon $fromDate = null,\n ?Carbon $toDate = null,\n DateTimeZone|string|null $timezone = 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($timezone);\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()->subWeek()->startOfWeek(CarbonInterface::MONDAY),\n 'toDate' => $now->copy()->subWeek()->endOfWeek(CarbonInterface::SUNDAY),\n ],\n self::FREQUENCY_MONTHLY => [\n 'fromDate' => $now->copy()->subMonthNoOverflow()->startOfMonth(),\n 'toDate' => $now->copy()->subMonthNoOverflow()->endOfMonth(),\n ],\n self::FREQUENCY_QUARTERLY => [\n 'fromDate' => $now->copy()->subQuarterNoOverflow()->startOfQuarter(),\n 'toDate' => $now->copy()->subQuarterNoOverflow()->endOfQuarter(),\n ],\n default => throw new InvalidArgumentException(\"Unsupported frequency: {$frequency}\"),\n };\n }\n\n private function calculateFromAndToDate(AutomatedReport $automatedReport): array\n {\n return $this->calculateFromAndToDatePeriod(\n $automatedReport->getFrequency(),\n $automatedReport->getFrom(),\n $automatedReport->getTo()\n );\n }\n\n public function getAskJiminnyGenerateReportPayload(\n AutomatedReport $automatedReport,\n AutomatedReportResult $reportResult,\n array $activityIds,\n ): array {\n return [\n 'user_question' => $automatedReport->getAskAnythingPrompt()?->getContent(),\n 'call_ids' => array_map('strval', $activityIds),\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResult->getUuid(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->getReportPeriodName($reportResult),\n 'report_name' => $automatedReport->getCustomName(),\n ];\n }\n\n private function getCallbackUrl(): string\n {\n return $this->webhookService->route('jiminny.webhook.reports.ready');\n }\n\n /**\n * Validate and transform payload data for automated reports\n *\n * @param array $data\n *\n * @throws InvalidArgumentException\n *\n * @return array\n */\n private function validateAndTransformData(array $data): array\n {\n // Validate organization (team) and check feature\n $team = $this->validateOrganization($data['organization'] ?? null);\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $type = $this->validateReportType($data['report_type'] ?? null);\n $frequency = $this->validateFrequency($data['frequency'] ?? null);\n $additionalPromptInput = $this->validateAdditionalPromptInput(\n $data['additional_prompt_input'] ?? null\n );\n $customReportName = $this->validateCustomReportName($data['custom_name'] ?? null);\n\n // Prepare data for the database\n $reportData = [\n 'team_id' => $team->getId(),\n 'type' => $type,\n 'status' => $status,\n 'frequency' => $frequency,\n 'additional_prompt_input' => $additionalPromptInput,\n 'custom_name' => $customReportName,\n ];\n\n // Validate deal values\n $reportData = $this->validateDealValues($data, $reportData);\n\n // Validate date range\n $reportData = $this->validateDateRange($data, $reportData, $frequency);\n\n // Validate call durations\n $reportData = $this->validateCallDurations($data, $reportData);\n\n // Validate call types\n $reportData = $this->validateCallTypes($data, $reportData);\n\n // Validate media types\n $reportData = $this->validateMediaTypes($data, $reportData);\n\n // Validate groups\n if (isset($data['teams'])) {\n $reportData['groups'] = $this->validateAndGetGroupIds($team, $data['teams']);\n }\n\n // Validate deal stages\n $reportData = $this->validateDealStages($data, $reportData, $team, $type);\n\n // Validate playbook categories\n $reportData = $this->validatePlaybookCategories($data, $reportData, $team);\n\n // Validate recipients\n $reportData['recipients'] = [\n 'users' => $this->validateAndGetUserIdsByTeam($team, $data['recipients'] ?? []),\n ];\n\n if (isset($data['jiminny_recipients'])) {\n // Validate Jiminny recipients\n $reportData['jiminny_recipients'] = [\n 'users' => $this->validateAndGetJiminnyUserIds((array) $data['jiminny_recipients']),\n ];\n }\n\n return $reportData;\n }\n\n private function validateDealValues(array $data, array $reportData): array\n {\n if (isset($data['min_deal_value'])) {\n $reportData['deal_value_min'] = (int) $data['min_deal_value'];\n\n if ($reportData['deal_value_min'] > 4294967295 || $reportData['deal_value_min'] < 0) {\n throw new InvalidArgumentException('Min deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_deal_value'])) {\n $reportData['deal_value_max'] = (int) $data['max_deal_value'];\n\n if ($reportData['deal_value_max'] > 4294967295 || $reportData['deal_value_max'] < 0) {\n throw new InvalidArgumentException('Max deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['min_deal_value'], $data['max_deal_value'])\n && $data['min_deal_value'] > $data['max_deal_value']\n ) {\n throw new InvalidArgumentException('Min deal value cannot be greater than max deal value');\n }\n\n return $reportData;\n }\n\n private function validateDateRange(array $data, array $reportData, string $frequency): array\n {\n // Set date range only for one_off frequency\n if ($frequency === 'one_off') {\n if (isset($data['start_date_period'])) {\n $reportData['from'] = $this->parseDate($data['start_date_period']);\n }\n\n if (isset($data['end_date_period'])) {\n $reportData['to'] = $this->parseDate($data['end_date_period']);\n }\n\n if (empty($reportData['from']) || empty($reportData['to'])) {\n throw new InvalidArgumentException(\n 'Start date and end date are required for one_off frequency'\n );\n }\n } else {\n $reportData['from'] = null;\n $reportData['to'] = null;\n }\n\n return $reportData;\n }\n\n private function validateCallDurations(array $data, array $reportData): array\n {\n // Convert call durations from minutes to seconds\n if (isset($data['min_call_duration'])) {\n $reportData['call_duration_min'] = (int) $data['min_call_duration'] * 60;\n\n if ($reportData['call_duration_min'] > 4294967295 || $reportData['call_duration_min'] < 0) {\n throw new InvalidArgumentException('Min call duration should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_call_duration'])) {\n $reportData['call_duration_max'] = (int) $data['max_call_duration'] * 60;\n\n if ($reportData['call_duration_max'] > 4294967295 || $reportData['call_duration_max'] < 0) {\n throw new InvalidArgumentException('Max call duration should be between 0 and 4294967295');\n }\n }\n\n return $reportData;\n }\n\n private function validateCallTypes(array $data, array $reportData): array\n {\n // Set call types\n $reportData['call_types'] = $data['call_type'] ?? [];\n if (empty($reportData['call_types'])) {\n $reportData['call_types'] = self::getCallTypes();\n }\n\n foreach ($reportData['call_types'] as $callType) {\n if (! in_array($callType, self::getCallTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Call type %s is invalid', $callType));\n }\n }\n\n return $reportData;\n }\n\n private function validateMediaTypes(array $data, array $reportData): array\n {\n // Set media types from input data\n $reportData['media_types'] = $data['media_types'] ?? [];\n\n if (empty($reportData['media_types'])) {\n throw new InvalidArgumentException('Media types are required');\n }\n\n foreach ($reportData['media_types'] as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n throw new InvalidArgumentException(sprintf('Media type %s is invalid', $mediaType));\n }\n }\n\n return $reportData;\n }\n\n private function validateDealStages(array $data, array $reportData, Team $team, string $reportType): array\n {\n // Validate and set deal stages\n if (isset($data['deal_stage_at_call'])) {\n $reportData['deal_at_call_stages'] =\n $this->validateAndGetDealStageIds($team, $data['deal_stage_at_call'], 'Deal stage at call');\n }\n\n if (isset($data['current_deal_stage'])) {\n $reportData['current_deal_stages'] =\n $this->validateAndGetDealStageIds($team, $data['current_deal_stage'], 'Current deal stage');\n }\n\n // Ensure current_deal_stage is not provided for loss_analysis report type\n if ($reportType === self::TYPE_LOSS_ANALYSIS && ! empty($data['current_deal_stage'])) {\n throw new InvalidArgumentException('Current deal stage is not applicable for Loss Analysis reports');\n }\n\n return $reportData;\n }\n\n // transform uuid to id\n private function validatePlaybookCategories(array $data, array $reportData, Team $team): array\n {\n $key = 'playbook_categories';\n\n if (isset($data[$key])) {\n $payloadIds = $data[$key];\n $ids = [];\n\n foreach ($payloadIds as $uuid) {\n $uuid = (string) $uuid;\n\n try {\n $playbookCategory = $this->playbookCategoryRepository->findByUuid($uuid);\n } catch (Throwable $throwable) {\n Log::error(__METHOD__ . ' ' . $throwable->getMessage());\n\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory) {\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory->hasPlaybook()) {\n throw new InvalidArgumentException(sprintf('Playbook category %s has no playbook', $uuid));\n }\n\n if ($playbookCategory->getPlaybook()->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Playbook category %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $ids[] = $playbookCategory->getId();\n }\n\n $reportData[$key] = $ids;\n }\n\n return $reportData;\n }\n\n private function validateReportStatus($status): bool\n {\n if (! in_array($status, [true, false], true)) {\n throw new InvalidArgumentException('Report status is invalid');\n }\n\n return $status;\n }\n\n private function validateReportType($type): string\n {\n if (! in_array($type, self::getTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Report type is invalid: %s', $type));\n }\n\n return $type;\n }\n\n private function validateFrequency($frequency): string\n {\n if (! in_array($frequency, self::getFrequencies(), true)) {\n throw new InvalidArgumentException('Frequency is invalid');\n }\n\n return $frequency;\n }\n\n private function validateAdditionalPromptInput(?string $additionalPromptInput): ?string\n {\n if ($additionalPromptInput && strlen($additionalPromptInput) > 5000) {\n throw new InvalidArgumentException('Additional Prompt Input should be less than 5000 characters');\n }\n\n return $additionalPromptInput;\n }\n\n private function validateCustomReportName(?string $customReportName): ?string\n {\n if ($customReportName === null || $customReportName === '') {\n return null;\n }\n\n if (strlen($customReportName) > 70) {\n throw new InvalidArgumentException('Custom report name should be less than 70 characters');\n }\n\n return $customReportName;\n }\n\n private function validateOrganization(?string $organizationUuid): Team\n {\n if (! $organizationUuid) {\n throw new InvalidArgumentException('Organization is required');\n }\n\n $team = $this->teamRepository->idOrUuid($organizationUuid);\n\n if (! $team) {\n throw new InvalidArgumentException('Organization not found');\n }\n\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n throw new InvalidArgumentException('Organization does not have the Automated Reports feature');\n }\n\n return $team;\n }\n\n private function validateAndGetGroupIds(Team $team, array $teamUuids): array\n {\n $groupIds = [];\n\n foreach ($teamUuids as $uuid) {\n $group = $this->groupRepository->findByUuid($uuid);\n\n if ($group === null || $group->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Group %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $groupIds[] = $group->getId();\n\n }\n\n return $groupIds;\n }\n\n private function validateAndGetDealStageIds(Team $team, array $stageUuids, string $propertyLabel): array\n {\n $stageIds = [];\n\n foreach ($stageUuids as $uuid) {\n $stage = $this->stageRepository->findByUuid($uuid);\n\n if ($stage === null || $stage->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Stage %s not found for team %s for %s', $uuid, $team->getUuid(), $propertyLabel)\n );\n }\n\n $stageIds[] = $stage->getId();\n }\n\n return $stageIds;\n }\n\n private function validateAndGetUserIds(array $userUuids, callable $teamCheck): array\n {\n if (empty($userUuids)) {\n return [];\n }\n\n $userIds = [];\n\n foreach ($userUuids as $uuid) {\n $user = $this->userRepository->findByUuid($uuid);\n\n if (! $user || ! $user->isStatusActive()) {\n throw new InvalidArgumentException(\n sprintf('User %s not found or is not active', $uuid)\n );\n }\n\n if (! $teamCheck($user)) {\n throw new InvalidArgumentException(\n sprintf('User %s does not belong to the allowed team(s)', $uuid)\n );\n }\n\n $userIds[] = $user->getId();\n }\n\n return $userIds;\n }\n\n private function validateAndGetUserIdsByTeam(Team $team, array $userUuids): array\n {\n return $this->validateAndGetUserIds($userUuids, fn ($user) => $user->getTeamId() === $team->getId());\n }\n\n private function validateAndGetJiminnyUserIds(array $userUuids): array\n {\n $allowedTeamIds = config('kiosk.teamIds', []);\n\n return $this->validateAndGetUserIds($userUuids, fn ($user) => in_array($user->getTeamId(), $allowedTeamIds, true));\n }\n\n private function parseDate(string $dateString): string\n {\n return date('Y-m-d H:i:s', strtotime($dateString));\n }\n\n private function generateReportResultViewUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.view', ['uuid' => $result->getUuid()]);\n }\n\n private function generateReportResultDownloadUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.download', ['uuid' => $result->getUuid()]);\n }\n\n private function getReportMediaRouteResource(AutomatedReportResult $result): string\n {\n if ($result->getMediaType() === self::MEDIA_TYPE_PDF) {\n return self::PDF_KEY;\n } elseif ($result->getMediaType() === self::MEDIA_TYPE_PODCAST) {\n return self::AUDIO_KEY;\n }\n\n throw new \\InvalidArgumentException('Unknown media type.');\n }\n\n public function getMediaPath(AutomatedReportResult $result): ?string\n {\n $url = match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => $result->getPdfUrl(),\n self::MEDIA_TYPE_PODCAST => $result->getPodcastAudioUrl(),\n default => null,\n };\n\n if ($url === null) {\n return null;\n }\n\n $path = parse_url(trim($url, '\"\\''), PHP_URL_PATH);\n\n return $path ?: null;\n }\n\n public function getFilenameSuffix(AutomatedReportResult $result): ?string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => 'Podcast',\n default => null,\n };\n }\n\n public function getMailSubjectSuffix(AutomatedReportResult $result): string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => 'report',\n self::MEDIA_TYPE_PODCAST => 'podcast',\n default => '',\n };\n }\n\n public function getMediaTypeMetadata(AutomatedReportResult $result): array\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => ['extension' => 'mp3', 'mime' => 'audio/mpeg'],\n self::MEDIA_TYPE_PDF => ['extension' => 'pdf', 'mime' => 'application/pdf'],\n default => ['extension' => null, 'mime' => null],\n };\n }\n\n public function deleteS3Files(AutomatedReportResult $result): void\n {\n $teamUuid = $result->getReport()->getTeam()->getUuid();\n $reportUuid = $result->getUuid();\n\n // delete all files for a report uuid no mather of pdf, podcast, or both\n // in case of both - the podcast files are linked to the pdf (parent) uuid\n // pdf and podcast date times should be close\n $path = sprintf('%s/%s/%s', $teamUuid, self::S3_DIR, $reportUuid);\n\n foreach (self::FILE_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted S3 file', [\n 'path' => $file,\n ]);\n }\n }\n\n foreach (self::FILE_PODCAST_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '_podcast.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted Podcast S3 file', [\n 'path' => $file,\n ]);\n }\n }\n }\n\n /**\n *\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): Collection\n {\n return $this->automatedReportsRepository->getTeamIdsWithReportsResults($teamId);\n }\n\n /**\n * Core delete logic for report results using a query\n *\n * @param Builder $query\n * @param array $logContext\n *\n * @return int\n */\n private function deleteReportResultsByQuery(Builder $query, array $logContext = []): int\n {\n $deletedCount = 0;\n\n if ($query->exists()) {\n Log::info(\n 'Run delete report results',\n array_merge(\n $logContext,\n [\n 'service' => 'AutomatedReportsService',\n ]\n )\n );\n\n $query->chunkById(50, function ($results) use (&$deletedCount, $logContext) {\n foreach ($results as $result) {\n $this->deleteReportResult($result);\n $deletedCount++;\n\n Log::info(\n 'Deleted a report result',\n array_merge(\n $logContext,\n [\n 'result_id' => $result->getId(),\n 'report_id' => $result->getReportId(),\n ]\n )\n );\n }\n });\n }\n\n return $deletedCount;\n }\n\n /**\n * Delete report results for a team by retention period\n *\n * @param Team $team\n * @param CarbonImmutable $retentionDate\n *\n * @return int Number of deleted report results\n */\n public function deleteReportsResultsInRetentionPeriod(Team $team, CarbonImmutable $retentionDate): int\n {\n $reportIds = $this->automatedReportsRepository->getReportIdsByTeam($team);\n\n if ($reportIds->isEmpty()) {\n return 0;\n }\n\n $query = $this->automatedReportsRepository\n ->getReportResultsQueryForRetention($team, $retentionDate);\n\n return $this->deleteReportResultsByQuery($query, [\n 'team_id' => $team->getId(),\n 'retention_date' => $retentionDate->toDateTimeString(),\n ]);\n }\n\n /**\n * Delete ALL report results for a specific automated report\n *\n * @param string $uuid\n *\n * @return int\n */\n public function deleteReportResults(string $uuid): int\n {\n $report = $this->getReport($uuid);\n\n $query = $this->automatedReportsRepository->getResultsByReportQuery($report);\n\n return $this->deleteReportResultsByQuery($query, [\n 'report_uuid' => $uuid,\n 'report_id' => $report->getId(),\n ]);\n }\n\n public function deleteReportResult(AutomatedReportResult $result): void\n {\n $this->deleteS3Files($result);\n\n $result->delete();\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getTeamReports(Team $team): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getReportsByTeam($team);\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getReportResults(AutomatedReport $report): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getResultsByReport($report);\n }\n\n public function deleteAllReportResults(AutomatedReport $report): void\n {\n $results = $this->getReportResults($report);\n\n /** @var AutomatedReportResult $result */\n foreach ($results as $result) {\n Log::info('Deleting result', [\n 'report' => $report->getId(),\n 'result' => $result->getId(),\n ]);\n\n $this->deleteReportResult($result);\n }\n }\n\n public function deleteAllData(Team $team): void\n {\n Log::info('Deleting automated report and results for team', [\n 'team' => $team->getId(),\n ]);\n\n $reports = $this->getTeamReports($team);\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n Log::info('Deleting report', [\n 'team' => $team->getId(),\n 'report' => $report->getId(),\n ]);\n\n $this->deleteAllReportResults($report);\n\n $report->delete();\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"21","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"18","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"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 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 * 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 `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\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;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","depth":4,"on_screen":true,"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 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 * 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 `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\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;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Socket fail to connect to host:address=(host=localhost)(port=3306)(type=primary). Connection refused","depth":3,"bounds":{"left":0.3263889,"top":0.0,"width":0.6125,"height":0.018888889},"on_screen":true,"value":"Socket fail to connect to host:address=(host=localhost)(port=3306)(type=primary). Connection refused","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1751808696220340334
|
1126710648141684156
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskAnythingPromptServiceTest
Run 'AskAnythingPromptServiceTest'
Debug 'AskAnythingPromptServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
100
3
34
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Kiosk\AutomatedReports;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Carbon\Exceptions\InvalidFormatException;
use DateTime;
use DateTimeInterface;
use DateTimeZone;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\ActivitySearch\FilterDefinition\InputTypeEnum;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Partner;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Throwable;
class AutomatedReportsService
{
public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';
public const string TYPE_ASK_JIMINNY = 'ask_jiminny';
/**
* Standard report types (used by kiosk for existing automated reports).
*/
// @TODO this will add filter, however if we need to control feature by FF we need conditional logic
public const array TYPES = [
['id' => 'exec_summary', 'name' => 'Exec Summary'],
['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],
['id' => 'product_feedback', 'name' => 'Product Feedback'],
['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],
// ['id' => 'questions', 'name' => 'Questions'],
// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],
];
public const array ALL_TYPES = [
...self::TYPES,
['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],
];
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';
/**
* Frequencies for standard (non-Ask Jiminny) reports.
*/
public const array FREQUENCIES = [
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
/**
* Frequencies for Ask Jiminny reports.
*/
public const array ASK_JIMINNY_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
];
public const string MEDIA_TYPE_PDF = 'pdf';
public const string MEDIA_TYPE_PODCAST = 'podcast';
public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];
public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];
public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];
public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];
public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];
public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];
public const int SENT_REPORT_AT_HOURS = 5;
public const string PDF_KEY = 'pdf';
public const string AUDIO_KEY = 'audio';
private const array ALL_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
private const string S3_DIR = 'reports';
private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];
private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly GroupRepository $groupRepository,
private readonly UserRepository $userRepository,
private readonly StageRepository $stageRepository,
private readonly DealStagesService $dealStagesService,
private readonly RecipientsService $recipientsService,
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly Webhook $webhookService,
private readonly BusDispatcher $dispatcher,
private readonly ActivityTypeService $activityTypeService,
private readonly PlaybookCategoryRepository $playbookCategoryRepository,
private readonly AskAnythingPromptService $askAnythingPromptService,
private readonly SearchRepository $activitySearchRepository,
private readonly AskAnythingRepository $askAnythingRepository,
) {
}
public static function getTypes(): array
{
$types = self::TYPES;
return array_map(static function ($type) {
return $type['id'];
}, $types);
}
public static function getCallTypes(): array
{
return array_map(static function ($callType) {
return $callType['id'];
}, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);
}
public static function getFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::FREQUENCIES);
}
// front-facing structure
public function getReportEnabledFieldData(bool $value = false): array
{
return [
'id' => 'report_enabled',
'label' => '',
'inputType' => InputTypeEnum::TOGGLE,
'value' => $value,
];
}
// Organizations = Teams
public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array
{
$options = $this->getTeams(partner: $partner);
if ($shortVersion) {
return [
'id' => 'organization',
'label' => 'Organization',
'options' => $options,
];
}
return [
'id' => 'organization',
'label' => 'Organization',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $options,
'value' => $value,
'dependencies' => [
'teams',
'deal_stage_at_call',
'current_deal_stage',
'recipients',
ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,
],
'dependsOn' => [],
];
}
// Teams = Groups
public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array
{
if ($shortVersion) {
return [
'id' => 'teams',
'label' => 'Team',
'options' => $options,
];
}
return [
'id' => 'teams',
'label' => 'Team',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => false,
'placeholder' => 'Select',
'options' => $options,
'value' => $value, // value should be an array of objects {id, name}
'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],
'dependsOn' => [],
];
}
public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array
{
$types = [];
if ($team instanceof Team) {
if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
$types = self::TYPES;
}
if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {
$types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];
}
} else {
$types = self::TYPES;
}
if ($shortVersion) {
return [
'id' => 'report_type',
'label' => 'Report Type',
'options' => $types,
];
}
return [
'id' => 'report_type',
'label' => 'Report Type',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $types,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getFrequencyFieldData(?string $value = null): array
{
return [
'id' => 'frequency',
'label' => 'Frequency',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => self::FREQUENCIES,
'value' => $value,
'dependencies' => ['period'],
'dependsOn' => [],
];
}
public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array
{
return [
'id' => 'period',
'label' => 'Select one-off period',
'inputType' => InputTypeEnum::DATE_RANGE,
'required' => true,
'placeholder' => 'Select',
'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],
'queryParams' => [
'startDate' => 'start_date_period',
'endDate' => 'end_date_period',
],
'dependencies' => [],
'dependsOn' => ['frequency'],
];
}
public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array
{
return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);
}
public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);
}
public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);
}
public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'deal_value',
'label' => 'Deal Value',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_deal_value',
'max' => 'max_deal_value',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array
{
$value = [];
$conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;
$dialerOn && $value[] = self::CALL_TYPE_DIALER;
return [
'id' => 'call_type',
'label' => 'Call Type',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => [
self::CALL_TYPE_CONFERENCE,
self::CALL_TYPE_DIALER,
],
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getMediaTypeFieldData(?AutomatedReport $report = null): array
{
$value = [];
if ($report) {
$value = $this->transformMediaTypes($report);
}
return [
'id' => 'media_types',
'label' => 'Export as',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => self::MEDIA_TYPE_OBJECTS,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'call_duration',
'label' => 'Call Duration',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_call_duration',
'max' => 'max_call_duration',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getRecipientsFieldData(?Team $team = null, array $value = []): array
{
return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);
}
public function getJiminnyRecipientsFieldData(array $value = []): array
{
return $this->recipientsService->getJiminnyRecipientsFieldData($value);
}
public function getAdditionalPromptInputFieldData(?string $value = null): array
{
return [
'id' => 'additional_prompt_input',
'label' => 'Special requirements',
'inputType' => InputTypeEnum::TEXTAREA,
'required' => false,
'placeholder' => 'What should be the focus of the report?',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCustomReportNameFieldData(?string $value = null): array
{
return [
'id' => 'custom_name',
'label' => 'Custom report name',
'inputType' => InputTypeEnum::TEXT,
'required' => false,
'placeholder' => 'Enter custom name',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
// data providers
public function getTeams(?Partner $partner = null): array
{
$teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);
$teamData = [];
foreach ($teams as $team) {
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
continue;
}
$teamData[] = $this->transformTeam($team);
}
return $teamData;
}
public function getTeamGroups(string $teamUuid): array
{
$data = [];
$team = $this->getTeam($teamUuid);
if ($team !== null) {
$groups = $team->groups()->get();
foreach ($groups as $group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
];
}
}
return $data;
}
public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array
{
$data = [];
$teams = $this->getTeams(partner: $partner);
foreach ($teams as $team) {
if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {
continue;
}
$data[] = [
'label' => $team['name'],
'groups' => $this->getTeamGroups($team['id']),
];
}
return $data;
}
public function getTeam(string $teamUuid): ?Team
{
return $this->teamRepository->idOrUuid($teamUuid);
}
public function getTeamById(int $teamId): ?Team
{
return $this->teamRepository->find($teamId);
}
public function getGroupsUuids(AutomatedReport $report): array
{
$uuids = [];
$reportGroups = $report->getGroups();
foreach ($reportGroups as $groupId) {
if ($group = $this->groupRepository->find($groupId)) {
$uuids[] = $group->getUuid();
}
}
return $uuids;
}
public function getPlaybookCategoriesUuids(AutomatedReport $report): array
{
$uuids = [];
$playbookCategories = $report->getPlaybookCategories();
foreach ($playbookCategories as $id) {
if ($category = $this->playbookCategoryRepository->find($id)) {
$uuids[] = $category->getUuid();
}
}
return $uuids;
}
public function getDealAtCallStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getDealAtCallStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getCurrentDealStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getCurrentDealStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getRecipients());
}
public function getJiminnyUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function extractUserUuids(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => $user->getUuid())
->values()
->all();
}
// get mail data
public function getRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getRecipients());
}
/**
* @return array<UserContract>
*/
public function getRecipientUserObjects(AutomatedReport $report): array
{
$userIds = $report->getRecipients()['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->values()
->all();
}
private function getJiminnyRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function buildRecipientUsers(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => [
'email' => $user->getEmailAddress(),
'name' => $user->getName(),
'timezone' => $user->getTimezone()->getName(),
])
->values()
->all();
}
public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array
{
if ($report->isAskJiminnyReport()) {
$recipients = $this->resolveAskJiminnyRecipients($report);
} else {
$recipients = $this->getRecipientUsers($report);
if ($includeJiminny) {
$recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));
}
}
$emails = [];
return array_values(array_filter(
$recipients,
static function ($recipient) use (&$emails) {
if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {
return false;
}
$emails[] = $recipient['email'];
return true;
}
));
}
private function resolveAskJiminnyRecipients(AutomatedReport $report): array
{
$recipients = [];
$creator = $report->getCreator();
if ($creator !== null) {
$recipients[] = [
'email' => $creator->getEmailAddress(),
'name' => $creator->getName(),
'timezone' => $creator->getTimezone()->getName(),
];
}
return array_merge(
$recipients,
$this->buildRecipientUsers($report->getRecipients()),
$this->getGroupRecipientUsers($report),
);
}
private function getGroupRecipientUsers(AutomatedReport $report): array
{
$users = [];
foreach ($report->getGroups() as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group === null) {
continue;
}
foreach ($group->getMembers() as $member) {
$users[] = [
'email' => $member->getEmailAddress(),
'name' => $member->getName(),
'timezone' => $member->getTimezone()->getName(),
];
}
}
return $users;
}
public function getReportTypeName(AutomatedReportResult $report): string
{
$type = $report->getReport()->getType();
$getType = $this->transformReportType($type);
return $getType['name'];
}
public function getReportPeriodName(AutomatedReportResult $report): string
{
$from = $report->getFromDate();
$to = $report->getToDate();
$frequency = $report->getReport()->getFrequency();
if ($from === null || $to === null) {
if (! $report->getReport()->isAskJiminnyReport()) {
$invalidPeriod = $from === null ? 'from' : 'to';
throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);
}
$timezone = $report->getReport()->getCreator()?->getTimezone();
$period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);
$from = $period['fromDate'];
$to = $period['toDate'];
}
return $this->formatReportPeriodName($frequency, $from, $to);
}
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 getReportTeamsName(AutomatedReportResult $report): string
{
$groups = $report->getGroups();
if (empty($groups)) {
return 'All';
}
// Get group names from repository
$groupNames = [];
foreach ($groups as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group) {
$groupNames[] = $group->getName();
}
}
if (count($groupNames) === 1) {
// Single team format
$teamsName = $groupNames[0];
} else {
// Multiple teams format
$teamsName = implode(', ', $groupNames);
}
return $teamsName;
}
public function getReportFileName(AutomatedReportResult $report): string
{
$customName = $report->getReport()->getCustomName();
$periodName = $this->getReportPeriodName($report);
$filenameSuffix = $this->getFilenameSuffix($report);
if ($customName) {
if ($filenameSuffix) {
$customName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$customName} - {$periodName}");
}
$baseName = $this->getReportTypeName($report);
if ($filenameSuffix) {
$baseName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}");
}
public function getReportFileNameWithExtension(AutomatedReportResult $result): string
{
$extension = $this->getMediaTypeMetadata($result)['extension'];
return $this->getReportFileName($result) . '.' . $extension;
}
public function sanitizeFileName(string $fileName): string
{
return str_replace(['/', '\\'], '-', $fileName);
}
public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool
{
$recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);
if (in_array($user->getId(), $recipientIds, true)) {
return true;
}
if ($report->isAskJiminnyReport()) {
$groupId = $user->getGroupId();
if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {
return true;
}
}
return false;
}
public function transformReportResults(Collection $automatedReportResults): array
{
$data = [];
foreach ($automatedReportResults as $automatedReportResult) {
/** @var AutomatedReportResult $automatedReportResult */
$report = $automatedReportResult->getReport();
$createdBy = $report->getCreator();
$creator = [
'id' => $createdBy?->getUuid(),
'name' => $createdBy?->getName(),
'email' => $createdBy?->getEmailAddress(),
'photoUrl' => $createdBy?->getPhotoUrl(),
];
$data[] = [
'id' => $automatedReportResult->getUuid(),
'name' => $automatedReportResult->getName(),
'frequency' => $this->transformFrequency($report->getFrequency()),
'recipients' => $this->buildRecipients($report),
'report_type' => $this->transformReportType($report->getType()),
'media_type' => $automatedReportResult->getMediaType(),
'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),
'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),
'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),
'creator' => $creator,
];
}
return $data;
}
private function buildRecipients(AutomatedReport $report): array
{
$creatorUuid = $report->getCreator()?->getUuid();
$recipients = array_values(array_filter(
$this->transformRecipients($report->getRecipients()),
static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,
));
if (! $report->isAskJiminnyReport()) {
return $recipients;
}
return [
...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),
...$recipients,
];
}
public function hasCallTypeConference(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);
}
public function hasCallTypeDialer(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);
}
// transformers
private function transformTeam(Team $team): array
{
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
return [];
}
return [
'id' => $team->getUuid(),
'name' => $team->getName(),
];
}
private function transformReportFullView(AutomatedReport $report): array
{
$base = $this->transformReportBase($report);
return $report->getType() === self::TYPE_ASK_JIMINNY
? $base + $this->transformAskJiminnyFields($report)
: $base + $this->transformStandardReportFields($report);
}
private function transformReportBase(AutomatedReport $report): array
{
return [
'id' => $report->getUuid(),
'organization' => $this->transformOrganization(team: $report->getTeam()),
'report_type' => $this->transformReportType($report->getType()),
'frequency' => $this->transformFrequency($report->getFrequency()),
];
}
private function transformStandardReportFields(AutomatedReport $report): array
{
$team = $report->getTeam();
return [
'report_enabled' => $report->getStatus(),
'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),
'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),
'deal_value_min' => $report->getDealValueMin(),
'deal_value_max' => $report->getDealValueMax(),
'call_types' => $this->transformCallType($report->getCallTypes()),
'media_types' => $this->transformMediaTypes($report),
'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),
'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),
'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),
'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),
'recipients' => $this->transformRecipients($report->getRecipients()),
'created_by' => $this->transformCreator($report->getCreator()),
'additional_prompt_input' => $report->getAdditionalPromptInput(),
'custom_name' => $report->getCustomName(),
'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),
'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),
'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),
];
}
private function transformAskJiminnyFields(AutomatedReport $report): array
{
$team = $report->getTeam();
$creatorId = $report->getAttribute('created_by');
$explicitUserIds = array_values(array_filter(
$report->getRecipients()['users'] ?? [],
static fn ($id) => $id !== $creatorId
));
return [
'report_name' => $report->getCustomName(),
'enabled' => $report->getStatus(),
'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),
'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),
'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),
'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),
];
}
private function transformOrganization(?Team $team): array
{
return [
'id' => $team?->getUuid(),
'name' => $team?->getName(),
];
}
private function transformReportType(string $type): array
{
foreach (self::ALL_TYPES as $typeItem) {
if ($typeItem['id'] === $type) {
return $typeItem;
}
}
return [];
}
private function transformCallType(array $types): array
{
$result = [];
$callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];
foreach ($types as $type) {
foreach ($callTypes as $callTypeItem) {
if ($callTypeItem['id'] === $type) {
$result[] = $callTypeItem;
break;
}
}
}
return $result;
}
private function transformMediaTypes(AutomatedReport $report): array
{
$values = [];
foreach ($report->getMediaTypes() as $mediaType) {
if (! in_array($mediaType, self::MEDIA_TYPES, true)) {
continue;
}
$values[] = match ($mediaType) {
self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,
self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,
};
}
return $values;
}
private function transformFrequency(string $frequency): array
{
foreach (self::ALL_FREQUENCIES as $frequencyItem) {
if ($frequencyItem['id'] === $frequency) {
return $frequencyItem;
}
}
return [];
}
public function transformDurationToMinutes(?int $duration): ?int
{
if (! $duration) {
return null;
}
return (int) ($duration / 60);
}
private function transformGroups(?Team $team, array $groupsIds): array
{
if (empty($groupsIds) || ! $team) {
return [];
}
$data = [];
foreach ($groupsIds as $groupId) {
$group = $team->groups()->where('id', $groupId)->first();
if ($group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
'photoUrl' => $group->getPhotoUrl(),
];
}
}
return $data;
}
private function transformStages(?Team $team, array $stagesIds): array
{
if (empty($stagesIds) || ! $team) {
return [];
}
$data = [];
foreach ($stagesIds as $stageId) {
$stage = $team->stages()->where('id', $stageId)->first();
if ($stage) {
$data[] = [
'id' => $stage->getUuid(),
'name' => $stage->getName(),
];
}
}
return $data;
}
private function transformRecipients(array $recipients): array
{
$users = [];
foreach ($recipients['users'] ?? [] as $userId) {
$users[] = $this->transformUser($userId);
}
return $users;
}
private function transformCreator(?User $user): ?array
{
if ($user === null) {
return null;
}
return $this->transformUser($user->getId());
}
private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array
{
if ($prompt === null) {
return null;
}
return [
'id' => $prompt->getUuid(),
'name' => $prompt->getTitle(),
];
}
private function transformSafeSearch(?Search $search): ?array
{
if ($search === null) {
return null;
}
return [
'id' => $search->getUuid(),
'name' => $search->getName(),
];
}
private function transformUser(int $userId): array
{
/* @var ?User $user */
$user = $this->userRepository->find($userId);
return [
'id' => $user?->getUuid(),
'name' => $user?->getName(),
'email' => $user?->getEmailAddress(),
'photoUrl' => $user?->getPhotoUrl(),
];
}
public function create(array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$validatedData['created_by'] = auth()->id();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
public function update(string $uuid, array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$report = $this->automatedReportsRepository->findByUuid($uuid);
if (! $report) {
throw new InvalidArgumentException('Report not found');
}
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
/**
* Create an Ask Jiminny report.
*/
public function createAskJiminnyReport(array $data, User $creator): array
{
$validatedData = $this->validateAskJiminnyReportData($data, $creator);
$validatedData['created_by'] = $creator->getId();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
return $this->transformReportFullView($automatedReport);
}
/**
* Update an Ask Jiminny report.
*/
public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array
{
if (! $report->isAskJiminnyReport()) {
throw new InvalidArgumentException('Report is not an Ask Jiminny report');
}
$validatedData = $this->validateAskJiminnyReportData($data, $user);
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
return $this->transformReportFullView($automatedReport);
}
public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array
{
if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {
throw new InvalidArgumentException(
'This report is missing a saved search or prompt. ' .
'Edit the report to complete the setup before enabling it.'
);
}
$this->automatedReportsRepository->update($report, ['status' => $status]);
return $this->transformReportFullView($report->fresh());
}
/**
* Validate and transform data for Ask Jiminny reports.
*/
private function validateAskJiminnyReportData(array $data, User $user): array
{
// Validate name
$name = trim($data['report_name'] ?? '');
if (empty($name)) {
throw new InvalidArgumentException('Report name is required');
}
if (mb_strlen($name) > 50) {
throw new InvalidArgumentException('Report name must be 50 characters or less');
}
// Validate frequency (only daily, weekly, monthly for Ask Jiminny)
$frequency = $data['frequency'] ?? null;
$askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];
if (! in_array($frequency, $askJiminnyFrequencies, true)) {
throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');
}
// Validate expiration date
$expiresAt = $data['expires_on'] ?? null;
if (empty($expiresAt)) {
throw new InvalidArgumentException('Expiration date is required');
}
try {
$expiresAtDate = Carbon::parse($expiresAt);
} catch (InvalidFormatException $e) {
throw new InvalidArgumentException('Expiration date format is invalid');
}
$maxExpiration = Carbon::now()->addYear()->endOfDay();
if ($expiresAtDate->gt($maxExpiration)) {
throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');
}
if ($expiresAtDate->isPast()) {
throw new InvalidArgumentException('Expiration date cannot be in the past');
}
// Validate saved search
$activitySearchId = $data['saved_search'] ?? null;
if (empty($activitySearchId)) {
throw new InvalidArgumentException('Saved search is required');
}
$savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);
if (! $savedSearch) {
throw new InvalidArgumentException('Saved search not found or does not belong to you');
}
// Validate saved prompt
$askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;
if (empty($askAnythingPromptId)) {
throw new InvalidArgumentException('Ask Jiminny prompt is required');
}
$prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);
if (! $prompt) {
throw new InvalidArgumentException('Ask Jiminny prompt not found');
}
// Validate status
$status = $data['enabled'] ?? false;
$recipientUserIds = [$user->getId()];
if (! empty($data['share_users'])) {
$sharedUserIds = $this->validateAndGetUserIdsByTeam(
$user->team,
(array) $data['share_users']
);
$recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);
}
$sharedGroupIds = [];
if (! empty($data['share_teams'])) {
$sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);
}
$recipientUserIds = array_values(array_unique($recipientUserIds));
return [
'team_id' => $user->getTeamId(),
'type' => self::TYPE_ASK_JIMINNY,
'status' => (bool) $status,
'frequency' => $frequency,
'custom_name' => $name,
'activity_search_id' => $savedSearch->getId(),
'ask_anything_prompt_id' => $prompt->getId(),
'expires_at' => $expiresAtDate->toDateString(),
'media_types' => [self::MEDIA_TYPE_PDF],
'call_types' => [],
'recipients' => ['users' => $recipientUserIds],
'groups' => $sharedGroupIds,
];
}
public static function getAskJiminnyFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::ASK_JIMINNY_FREQUENCIES);
}
public function getAskJiminnyReportFilters(User $user): array
{
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
return [
[
'id' => 'prompt',
'label' => 'Prompt',
'options' => $prompts,
],
[
'id' => 'saved_search',
'label' => 'Saved Search',
'options' => $savedSearches,
],
];
}
public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array
{
$team = $user->getTeam();
$userTimezone = $user->getTimezone();
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
$teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [
'id' => $group->getUuid(),
'name' => $group->getName(),
])->values()->all();
$shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];
$sharedTeamsValue = [];
$sharedUsersValue = [];
if ($report) {
$sharedTeamsValue = $this->transformGroups($team, $report->getGroups());
$recipientUserIds = $report->getRecipients()['users'] ?? [];
$creatorId = $report->getAttribute('created_by');
$sharedUserIds = array_values(array_filter(
$recipientUserIds,
static fn ($id) => $id !== $creatorId
));
$sharedUsersValue = collect($sharedUserIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (User $u) => [
'id' => $u->getUuid(),
'name' => $u->getName(),
])
->values()
->all();
}
return [
'fields' => [
[
'id' => 'enabled',
'inputType' => InputTypeEnum::TOGGLE,
'label' => '',
'value' => $report?->getStatus() ?? false,
],
[
'id' => 'report_name',
'inputType' => InputTypeEnum::TEXT,
'label' => 'Name',
'placeholder' => 'Enter name',
'required' => true,
'validation' => ['maxLength' => 50],
'value' => $report?->getCustomName() ?? '',
...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58020
|
2043
|
2
|
2026-05-19T11:27:18.088215+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190038088_m2.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskAnythingPromptServiceTest
Run 'AskAnythingPromptServiceTest'
Debug 'AskAnythingPromptServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
100
3
34
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Kiosk\AutomatedReports;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Carbon\Exceptions\InvalidFormatException;
use DateTime;
use DateTimeInterface;
use DateTimeZone;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\ActivitySearch\FilterDefinition\InputTypeEnum;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Partner;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Throwable;
class AutomatedReportsService
{
public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';
public const string TYPE_ASK_JIMINNY = 'ask_jiminny';
/**
* Standard report types (used by kiosk for existing automated reports).
*/
// @TODO this will add filter, however if we need to control feature by FF we need conditional logic
public const array TYPES = [
['id' => 'exec_summary', 'name' => 'Exec Summary'],
['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],
['id' => 'product_feedback', 'name' => 'Product Feedback'],
['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],
// ['id' => 'questions', 'name' => 'Questions'],
// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],
];
public const array ALL_TYPES = [
...self::TYPES,
['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],
];
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';
/**
* Frequencies for standard (non-Ask Jiminny) reports.
*/
public const array FREQUENCIES = [
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
/**
* Frequencies for Ask Jiminny reports.
*/
public const array ASK_JIMINNY_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
];
public const string MEDIA_TYPE_PDF = 'pdf';
public const string MEDIA_TYPE_PODCAST = 'podcast';
public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];
public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];
public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];
public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];
public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];
public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];
public const int SENT_REPORT_AT_HOURS = 5;
public const string PDF_KEY = 'pdf';
public const string AUDIO_KEY = 'audio';
private const array ALL_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
private const string S3_DIR = 'reports';
private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];
private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly GroupRepository $groupRepository,
private readonly UserRepository $userRepository,
private readonly StageRepository $stageRepository,
private readonly DealStagesService $dealStagesService,
private readonly RecipientsService $recipientsService,
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly Webhook $webhookService,
private readonly BusDispatcher $dispatcher,
private readonly ActivityTypeService $activityTypeService,
private readonly PlaybookCategoryRepository $playbookCategoryRepository,
private readonly AskAnythingPromptService $askAnythingPromptService,
private readonly SearchRepository $activitySearchRepository,
private readonly AskAnythingRepository $askAnythingRepository,
) {
}
public static function getTypes(): array
{
$types = self::TYPES;
return array_map(static function ($type) {
return $type['id'];
}, $types);
}
public static function getCallTypes(): array
{
return array_map(static function ($callType) {
return $callType['id'];
}, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);
}
public static function getFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::FREQUENCIES);
}
// front-facing structure
public function getReportEnabledFieldData(bool $value = false): array
{
return [
'id' => 'report_enabled',
'label' => '',
'inputType' => InputTypeEnum::TOGGLE,
'value' => $value,
];
}
// Organizations = Teams
public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array
{
$options = $this->getTeams(partner: $partner);
if ($shortVersion) {
return [
'id' => 'organization',
'label' => 'Organization',
'options' => $options,
];
}
return [
'id' => 'organization',
'label' => 'Organization',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $options,
'value' => $value,
'dependencies' => [
'teams',
'deal_stage_at_call',
'current_deal_stage',
'recipients',
ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,
],
'dependsOn' => [],
];
}
// Teams = Groups
public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array
{
if ($shortVersion) {
return [
'id' => 'teams',
'label' => 'Team',
'options' => $options,
];
}
return [
'id' => 'teams',
'label' => 'Team',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => false,
'placeholder' => 'Select',
'options' => $options,
'value' => $value, // value should be an array of objects {id, name}
'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],
'dependsOn' => [],
];
}
public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array
{
$types = [];
if ($team instanceof Team) {
if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
$types = self::TYPES;
}
if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {
$types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];
}
} else {
$types = self::TYPES;
}
if ($shortVersion) {
return [
'id' => 'report_type',
'label' => 'Report Type',
'options' => $types,
];
}
return [
'id' => 'report_type',
'label' => 'Report Type',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $types,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getFrequencyFieldData(?string $value = null): array
{
return [
'id' => 'frequency',
'label' => 'Frequency',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => self::FREQUENCIES,
'value' => $value,
'dependencies' => ['period'],
'dependsOn' => [],
];
}
public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array
{
return [
'id' => 'period',
'label' => 'Select one-off period',
'inputType' => InputTypeEnum::DATE_RANGE,
'required' => true,
'placeholder' => 'Select',
'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],
'queryParams' => [
'startDate' => 'start_date_period',
'endDate' => 'end_date_period',
],
'dependencies' => [],
'dependsOn' => ['frequency'],
];
}
public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array
{
return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);
}
public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);
}
public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);
}
public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'deal_value',
'label' => 'Deal Value',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_deal_value',
'max' => 'max_deal_value',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array
{
$value = [];
$conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;
$dialerOn && $value[] = self::CALL_TYPE_DIALER;
return [
'id' => 'call_type',
'label' => 'Call Type',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => [
self::CALL_TYPE_CONFERENCE,
self::CALL_TYPE_DIALER,
],
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getMediaTypeFieldData(?AutomatedReport $report = null): array
{
$value = [];
if ($report) {
$value = $this->transformMediaTypes($report);
}
return [
'id' => 'media_types',
'label' => 'Export as',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => self::MEDIA_TYPE_OBJECTS,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'call_duration',
'label' => 'Call Duration',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_call_duration',
'max' => 'max_call_duration',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getRecipientsFieldData(?Team $team = null, array $value = []): array
{
return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);
}
public function getJiminnyRecipientsFieldData(array $value = []): array
{
return $this->recipientsService->getJiminnyRecipientsFieldData($value);
}
public function getAdditionalPromptInputFieldData(?string $value = null): array
{
return [
'id' => 'additional_prompt_input',
'label' => 'Special requirements',
'inputType' => InputTypeEnum::TEXTAREA,
'required' => false,
'placeholder' => 'What should be the focus of the report?',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCustomReportNameFieldData(?string $value = null): array
{
return [
'id' => 'custom_name',
'label' => 'Custom report name',
'inputType' => InputTypeEnum::TEXT,
'required' => false,
'placeholder' => 'Enter custom name',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
// data providers
public function getTeams(?Partner $partner = null): array
{
$teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);
$teamData = [];
foreach ($teams as $team) {
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
continue;
}
$teamData[] = $this->transformTeam($team);
}
return $teamData;
}
public function getTeamGroups(string $teamUuid): array
{
$data = [];
$team = $this->getTeam($teamUuid);
if ($team !== null) {
$groups = $team->groups()->get();
foreach ($groups as $group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
];
}
}
return $data;
}
public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array
{
$data = [];
$teams = $this->getTeams(partner: $partner);
foreach ($teams as $team) {
if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {
continue;
}
$data[] = [
'label' => $team['name'],
'groups' => $this->getTeamGroups($team['id']),
];
}
return $data;
}
public function getTeam(string $teamUuid): ?Team
{
return $this->teamRepository->idOrUuid($teamUuid);
}
public function getTeamById(int $teamId): ?Team
{
return $this->teamRepository->find($teamId);
}
public function getGroupsUuids(AutomatedReport $report): array
{
$uuids = [];
$reportGroups = $report->getGroups();
foreach ($reportGroups as $groupId) {
if ($group = $this->groupRepository->find($groupId)) {
$uuids[] = $group->getUuid();
}
}
return $uuids;
}
public function getPlaybookCategoriesUuids(AutomatedReport $report): array
{
$uuids = [];
$playbookCategories = $report->getPlaybookCategories();
foreach ($playbookCategories as $id) {
if ($category = $this->playbookCategoryRepository->find($id)) {
$uuids[] = $category->getUuid();
}
}
return $uuids;
}
public function getDealAtCallStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getDealAtCallStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getCurrentDealStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getCurrentDealStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getRecipients());
}
public function getJiminnyUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function extractUserUuids(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => $user->getUuid())
->values()
->all();
}
// get mail data
public function getRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getRecipients());
}
/**
* @return array<UserContract>
*/
public function getRecipientUserObjects(AutomatedReport $report): array
{
$userIds = $report->getRecipients()['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->values()
->all();
}
private function getJiminnyRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function buildRecipientUsers(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => [
'email' => $user->getEmailAddress(),
'name' => $user->getName(),
'timezone' => $user->getTimezone()->getName(),
])
->values()
->all();
}
public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array
{
if ($report->isAskJiminnyReport()) {
$recipients = $this->resolveAskJiminnyRecipients($report);
} else {
$recipients = $this->getRecipientUsers($report);
if ($includeJiminny) {
$recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));
}
}
$emails = [];
return array_values(array_filter(
$recipients,
static function ($recipient) use (&$emails) {
if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {
return false;
}
$emails[] = $recipient['email'];
return true;
}
));
}
private function resolveAskJiminnyRecipients(AutomatedReport $report): array
{
$recipients = [];
$creator = $report->getCreator();
if ($creator !== null) {
$recipients[] = [
'email' => $creator->getEmailAddress(),
'name' => $creator->getName(),
'timezone' => $creator->getTimezone()->getName(),
];
}
return array_merge(
$recipients,
$this->buildRecipientUsers($report->getRecipients()),
$this->getGroupRecipientUsers($report),
);
}
private function getGroupRecipientUsers(AutomatedReport $report): array
{
$users = [];
foreach ($report->getGroups() as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group === null) {
continue;
}
foreach ($group->getMembers() as $member) {
$users[] = [
'email' => $member->getEmailAddress(),
'name' => $member->getName(),
'timezone' => $member->getTimezone()->getName(),
];
}
}
return $users;
}
public function getReportTypeName(AutomatedReportResult $report): string
{
$type = $report->getReport()->getType();
$getType = $this->transformReportType($type);
return $getType['name'];
}
public function getReportPeriodName(AutomatedReportResult $report): string
{
$from = $report->getFromDate();
$to = $report->getToDate();
$frequency = $report->getReport()->getFrequency();
if ($from === null || $to === null) {
if (! $report->getReport()->isAskJiminnyReport()) {
$invalidPeriod = $from === null ? 'from' : 'to';
throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);
}
$timezone = $report->getReport()->getCreator()?->getTimezone();
$period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);
$from = $period['fromDate'];
$to = $period['toDate'];
}
return $this->formatReportPeriodName($frequency, $from, $to);
}
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 getReportTeamsName(AutomatedReportResult $report): string
{
$groups = $report->getGroups();
if (empty($groups)) {
return 'All';
}
// Get group names from repository
$groupNames = [];
foreach ($groups as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group) {
$groupNames[] = $group->getName();
}
}
if (count($groupNames) === 1) {
// Single team format
$teamsName = $groupNames[0];
} else {
// Multiple teams format
$teamsName = implode(', ', $groupNames);
}
return $teamsName;
}
public function getReportFileName(AutomatedReportResult $report): string
{
$customName = $report->getReport()->getCustomName();
$periodName = $this->getReportPeriodName($report);
$filenameSuffix = $this->getFilenameSuffix($report);
if ($customName) {
if ($filenameSuffix) {
$customName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$customName} - {$periodName}");
}
$baseName = $this->getReportTypeName($report);
if ($filenameSuffix) {
$baseName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}");
}
public function getReportFileNameWithExtension(AutomatedReportResult $result): string
{
$extension = $this->getMediaTypeMetadata($result)['extension'];
return $this->getReportFileName($result) . '.' . $extension;
}
public function sanitizeFileName(string $fileName): string
{
return str_replace(['/', '\\'], '-', $fileName);
}
public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool
{
$recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);
if (in_array($user->getId(), $recipientIds, true)) {
return true;
}
if ($report->isAskJiminnyReport()) {
$groupId = $user->getGroupId();
if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {
return true;
}
}
return false;
}
public function transformReportResults(Collection $automatedReportResults): array
{
$data = [];
foreach ($automatedReportResults as $automatedReportResult) {
/** @var AutomatedReportResult $automatedReportResult */
$report = $automatedReportResult->getReport();
$createdBy = $report->getCreator();
$creator = [
'id' => $createdBy?->getUuid(),
'name' => $createdBy?->getName(),
'email' => $createdBy?->getEmailAddress(),
'photoUrl' => $createdBy?->getPhotoUrl(),
];
$data[] = [
'id' => $automatedReportResult->getUuid(),
'name' => $automatedReportResult->getName(),
'frequency' => $this->transformFrequency($report->getFrequency()),
'recipients' => $this->buildRecipients($report),
'report_type' => $this->transformReportType($report->getType()),
'media_type' => $automatedReportResult->getMediaType(),
'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),
'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),
'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),
'creator' => $creator,
];
}
return $data;
}
private function buildRecipients(AutomatedReport $report): array
{
$creatorUuid = $report->getCreator()?->getUuid();
$recipients = array_values(array_filter(
$this->transformRecipients($report->getRecipients()),
static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,
));
if (! $report->isAskJiminnyReport()) {
return $recipients;
}
return [
...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),
...$recipients,
];
}
public function hasCallTypeConference(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);
}
public function hasCallTypeDialer(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);
}
// transformers
private function transformTeam(Team $team): array
{
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
return [];
}
return [
'id' => $team->getUuid(),
'name' => $team->getName(),
];
}
private function transformReportFullView(AutomatedReport $report): array
{
$base = $this->transformReportBase($report);
return $report->getType() === self::TYPE_ASK_JIMINNY
? $base + $this->transformAskJiminnyFields($report)
: $base + $this->transformStandardReportFields($report);
}
private function transformReportBase(AutomatedReport $report): array
{
return [
'id' => $report->getUuid(),
'organization' => $this->transformOrganization(team: $report->getTeam()),
'report_type' => $this->transformReportType($report->getType()),
'frequency' => $this->transformFrequency($report->getFrequency()),
];
}
private function transformStandardReportFields(AutomatedReport $report): array
{
$team = $report->getTeam();
return [
'report_enabled' => $report->getStatus(),
'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),
'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),
'deal_value_min' => $report->getDealValueMin(),
'deal_value_max' => $report->getDealValueMax(),
'call_types' => $this->transformCallType($report->getCallTypes()),
'media_types' => $this->transformMediaTypes($report),
'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),
'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),
'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),
'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),
'recipients' => $this->transformRecipients($report->getRecipients()),
'created_by' => $this->transformCreator($report->getCreator()),
'additional_prompt_input' => $report->getAdditionalPromptInput(),
'custom_name' => $report->getCustomName(),
'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),
'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),
'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),
];
}
private function transformAskJiminnyFields(AutomatedReport $report): array
{
$team = $report->getTeam();
$creatorId = $report->getAttribute('created_by');
$explicitUserIds = array_values(array_filter(
$report->getRecipients()['users'] ?? [],
static fn ($id) => $id !== $creatorId
));
return [
'report_name' => $report->getCustomName(),
'enabled' => $report->getStatus(),
'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),
'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),
'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),
'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),
];
}
private function transformOrganization(?Team $team): array
{
return [
'id' => $team?->getUuid(),
'name' => $team?->getName(),
];
}
private function transformReportType(string $type): array
{
foreach (self::ALL_TYPES as $typeItem) {
if ($typeItem['id'] === $type) {
return $typeItem;
}
}
return [];
}
private function transformCallType(array $types): array
{
$result = [];
$callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];
foreach ($types as $type) {
foreach ($callTypes as $callTypeItem) {
if ($callTypeItem['id'] === $type) {
$result[] = $callTypeItem;
break;
}
}
}
return $result;
}
private function transformMediaTypes(AutomatedReport $report): array
{
$values = [];
foreach ($report->getMediaTypes() as $mediaType) {
if (! in_array($mediaType, self::MEDIA_TYPES, true)) {
continue;
}
$values[] = match ($mediaType) {
self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,
self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,
};
}
return $values;
}
private function transformFrequency(string $frequency): array
{
foreach (self::ALL_FREQUENCIES as $frequencyItem) {
if ($frequencyItem['id'] === $frequency) {
return $frequencyItem;
}
}
return [];
}
public function transformDurationToMinutes(?int $duration): ?int
{
if (! $duration) {
return null;
}
return (int) ($duration / 60);
}
private function transformGroups(?Team $team, array $groupsIds): array
{
if (empty($groupsIds) || ! $team) {
return [];
}
$data = [];
foreach ($groupsIds as $groupId) {
$group = $team->groups()->where('id', $groupId)->first();
if ($group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
'photoUrl' => $group->getPhotoUrl(),
];
}
}
return $data;
}
private function transformStages(?Team $team, array $stagesIds): array
{
if (empty($stagesIds) || ! $team) {
return [];
}
$data = [];
foreach ($stagesIds as $stageId) {
$stage = $team->stages()->where('id', $stageId)->first();
if ($stage) {
$data[] = [
'id' => $stage->getUuid(),
'name' => $stage->getName(),
];
}
}
return $data;
}
private function transformRecipients(array $recipients): array
{
$users = [];
foreach ($recipients['users'] ?? [] as $userId) {
$users[] = $this->transformUser($userId);
}
return $users;
}
private function transformCreator(?User $user): ?array
{
if ($user === null) {
return null;
}
return $this->transformUser($user->getId());
}
private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array
{
if ($prompt === null) {
return null;
}
return [
'id' => $prompt->getUuid(),
'name' => $prompt->getTitle(),
];
}
private function transformSafeSearch(?Search $search): ?array
{
if ($search === null) {
return null;
}
return [
'id' => $search->getUuid(),
'name' => $search->getName(),
];
}
private function transformUser(int $userId): array
{
/* @var ?User $user */
$user = $this->userRepository->find($userId);
return [
'id' => $user?->getUuid(),
'name' => $user?->getName(),
'email' => $user?->getEmailAddress(),
'photoUrl' => $user?->getPhotoUrl(),
];
}
public function create(array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$validatedData['created_by'] = auth()->id();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
public function update(string $uuid, array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$report = $this->automatedReportsRepository->findByUuid($uuid);
if (! $report) {
throw new InvalidArgumentException('Report not found');
}
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
/**
* Create an Ask Jiminny report.
*/
public function createAskJiminnyReport(array $data, User $creator): array
{
$validatedData = $this->validateAskJiminnyReportData($data, $creator);
$validatedData['created_by'] = $creator->getId();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
return $this->transformReportFullView($automatedReport);
}
/**
* Update an Ask Jiminny report.
*/
public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array
{
if (! $report->isAskJiminnyReport()) {
throw new InvalidArgumentException('Report is not an Ask Jiminny report');
}
$validatedData = $this->validateAskJiminnyReportData($data, $user);
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
return $this->transformReportFullView($automatedReport);
}
public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array
{
if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {
throw new InvalidArgumentException(
'This report is missing a saved search or prompt. ' .
'Edit the report to complete the setup before enabling it.'
);
}
$this->automatedReportsRepository->update($report, ['status' => $status]);
return $this->transformReportFullView($report->fresh());
}
/**
* Validate and transform data for Ask Jiminny reports.
*/
private function validateAskJiminnyReportData(array $data, User $user): array
{
// Validate name
$name = trim($data['report_name'] ?? '');
if (empty($name)) {
throw new InvalidArgumentException('Report name is required');
}
if (mb_strlen($name) > 50) {
throw new InvalidArgumentException('Report name must be 50 characters or less');
}
// Validate frequency (only daily, weekly, monthly for Ask Jiminny)
$frequency = $data['frequency'] ?? null;
$askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];
if (! in_array($frequency, $askJiminnyFrequencies, true)) {
throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');
}
// Validate expiration date
$expiresAt = $data['expires_on'] ?? null;
if (empty($expiresAt)) {
throw new InvalidArgumentException('Expiration date is required');
}
try {
$expiresAtDate = Carbon::parse($expiresAt);
} catch (InvalidFormatException $e) {
throw new InvalidArgumentException('Expiration date format is invalid');
}
$maxExpiration = Carbon::now()->addYear()->endOfDay();
if ($expiresAtDate->gt($maxExpiration)) {
throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');
}
if ($expiresAtDate->isPast()) {
throw new InvalidArgumentException('Expiration date cannot be in the past');
}
// Validate saved search
$activitySearchId = $data['saved_search'] ?? null;
if (empty($activitySearchId)) {
throw new InvalidArgumentException('Saved search is required');
}
$savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);
if (! $savedSearch) {
throw new InvalidArgumentException('Saved search not found or does not belong to you');
}
// Validate saved prompt
$askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;
if (empty($askAnythingPromptId)) {
throw new InvalidArgumentException('Ask Jiminny prompt is required');
}
$prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);
if (! $prompt) {
throw new InvalidArgumentException('Ask Jiminny prompt not found');
}
// Validate status
$status = $data['enabled'] ?? false;
$recipientUserIds = [$user->getId()];
if (! empty($data['share_users'])) {
$sharedUserIds = $this->validateAndGetUserIdsByTeam(
$user->team,
(array) $data['share_users']
);
$recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);
}
$sharedGroupIds = [];
if (! empty($data['share_teams'])) {
$sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);
}
$recipientUserIds = array_values(array_unique($recipientUserIds));
return [
'team_id' => $user->getTeamId(),
'type' => self::TYPE_ASK_JIMINNY,
'status' => (bool) $status,
'frequency' => $frequency,
'custom_name' => $name,
'activity_search_id' => $savedSearch->getId(),
'ask_anything_prompt_id' => $prompt->getId(),
'expires_at' => $expiresAtDate->toDateString(),
'media_types' => [self::MEDIA_TYPE_PDF],
'call_types' => [],
'recipients' => ['users' => $recipientUserIds],
'groups' => $sharedGroupIds,
];
}
public static function getAskJiminnyFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::ASK_JIMINNY_FREQUENCIES);
}
public function getAskJiminnyReportFilters(User $user): array
{
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
return [
[
'id' => 'prompt',
'label' => 'Prompt',
'options' => $prompts,
],
[
'id' => 'saved_search',
'label' => 'Saved Search',
'options' => $savedSearches,
],
];
}
public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array
{
$team = $user->getTeam();
$userTimezone = $user->getTimezone();
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
$teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [
'id' => $group->getUuid(),
'name' => $group->getName(),
])->values()->all();
$shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];
$sharedTeamsValue = [];
$sharedUsersValue = [];
if ($report) {
$sharedTeamsValue = $this->transformGroups($team, $report->getGroups());
$recipientUserIds = $report->getRecipients()['users'] ?? [];
$creatorId = $report->getAttribute('created_by');
$sharedUserIds = array_values(array_filter(
$recipientUserIds,
static fn ($id) => $id !== $creatorId
));
$sharedUsersValue = collect($sharedUserIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (User $u) => [
'id' => $u->getUuid(),
'name' => $u->getName(),
])
->values()
->all();
}
return [
'fields' => [
[
'id' => 'enabled',
'inputType' => InputTypeEnum::TOGGLE,
'label' => '',
'value' => $report?->getStatus() ?? false,
],
[
'id' => 'report_name',
'inputType' => InputTypeEnum::TEXT,
'label' => 'Name',
'placeholder' => 'Enter name',
'required' => true,
'validation' => ['maxLength' => 50],
'value' => $report?->getCustomName() ?? '',
...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.10405585,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects<br/>Some incoming commits are not fetched<br/>","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.8194814,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskAnythingPromptServiceTest","depth":6,"bounds":{"left":0.83477396,"top":0.019952115,"width":0.080784574,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskAnythingPromptServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskAnythingPromptServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"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},"on_screen":true,"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},"on_screen":true,"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},"on_screen":true,"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},"on_screen":true,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"100","depth":4,"bounds":{"left":0.37599733,"top":0.15003991,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.3899601,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"34","depth":4,"bounds":{"left":0.39993352,"top":0.15003991,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.4192154,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"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\\Services\\Kiosk\\AutomatedReports;\n\nuse Carbon\\CarbonImmutable;\nuse Carbon\\CarbonInterface;\nuse Carbon\\Exceptions\\InvalidFormatException;\nuse DateTime;\nuse DateTimeInterface;\nuse DateTimeZone;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\InputTypeEnum;\nuse Jiminny\\Component\\AskAnything\\AskAnythingPromptService;\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Contracts\\Repositories\\PlaybookCategoryRepository;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\ModelNotFoundException;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Partner;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Repositories\\GroupRepository;\nuse Jiminny\\Repositories\\SearchRepository;\nuse Jiminny\\Repositories\\StageRepository;\nuse Throwable;\n\nclass AutomatedReportsService\n{\n public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';\n public const string TYPE_ASK_JIMINNY = 'ask_jiminny';\n\n /**\n * Standard report types (used by kiosk for existing automated reports).\n */\n // @TODO this will add filter, however if we need to control feature by FF we need conditional logic\n public const array TYPES = [\n ['id' => 'exec_summary', 'name' => 'Exec Summary'],\n ['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],\n ['id' => 'product_feedback', 'name' => 'Product Feedback'],\n ['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],\n// ['id' => 'questions', 'name' => 'Questions'],\n// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],\n ];\n\n public const array ALL_TYPES = [\n ...self::TYPES,\n ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],\n ];\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\n /**\n * Frequencies for standard (non-Ask Jiminny) reports.\n */\n public const array FREQUENCIES = [\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n\n /**\n * Frequencies for Ask Jiminny reports.\n */\n public const array ASK_JIMINNY_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ];\n\n public const string MEDIA_TYPE_PDF = 'pdf';\n public const string MEDIA_TYPE_PODCAST = 'podcast';\n public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];\n public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];\n public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];\n public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];\n\n public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];\n public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];\n public const int SENT_REPORT_AT_HOURS = 5;\n public const string PDF_KEY = 'pdf';\n public const string AUDIO_KEY = 'audio';\n\n private const array ALL_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n private const string S3_DIR = 'reports';\n private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];\n private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];\n\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly GroupRepository $groupRepository,\n private readonly UserRepository $userRepository,\n private readonly StageRepository $stageRepository,\n private readonly DealStagesService $dealStagesService,\n private readonly RecipientsService $recipientsService,\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly Webhook $webhookService,\n private readonly BusDispatcher $dispatcher,\n private readonly ActivityTypeService $activityTypeService,\n private readonly PlaybookCategoryRepository $playbookCategoryRepository,\n private readonly AskAnythingPromptService $askAnythingPromptService,\n private readonly SearchRepository $activitySearchRepository,\n private readonly AskAnythingRepository $askAnythingRepository,\n ) {\n }\n\n public static function getTypes(): array\n {\n $types = self::TYPES;\n\n return array_map(static function ($type) {\n return $type['id'];\n }, $types);\n }\n\n public static function getCallTypes(): array\n {\n return array_map(static function ($callType) {\n return $callType['id'];\n }, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);\n }\n\n public static function getFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::FREQUENCIES);\n }\n\n // front-facing structure\n public function getReportEnabledFieldData(bool $value = false): array\n {\n return [\n 'id' => 'report_enabled',\n 'label' => '',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'value' => $value,\n ];\n }\n\n // Organizations = Teams\n public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array\n {\n $options = $this->getTeams(partner: $partner);\n\n if ($shortVersion) {\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value,\n 'dependencies' => [\n 'teams',\n 'deal_stage_at_call',\n 'current_deal_stage',\n 'recipients',\n ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,\n ],\n 'dependsOn' => [],\n ];\n }\n\n // Teams = Groups\n public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array\n {\n if ($shortVersion) {\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value, // value should be an array of objects {id, name}\n 'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],\n 'dependsOn' => [],\n ];\n }\n\n public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array\n {\n $types = [];\n if ($team instanceof Team) {\n if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n $types = self::TYPES;\n }\n if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {\n $types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];\n }\n } else {\n $types = self::TYPES;\n }\n\n if ($shortVersion) {\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'options' => $types,\n ];\n }\n\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $types,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getFrequencyFieldData(?string $value = null): array\n {\n return [\n 'id' => 'frequency',\n 'label' => 'Frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::FREQUENCIES,\n 'value' => $value,\n 'dependencies' => ['period'],\n 'dependsOn' => [],\n ];\n }\n\n public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array\n {\n return [\n 'id' => 'period',\n 'label' => 'Select one-off period',\n 'inputType' => InputTypeEnum::DATE_RANGE,\n 'required' => true,\n 'placeholder' => 'Select',\n 'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],\n 'queryParams' => [\n 'startDate' => 'start_date_period',\n 'endDate' => 'end_date_period',\n ],\n 'dependencies' => [],\n 'dependsOn' => ['frequency'],\n ];\n }\n\n public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array\n {\n return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);\n }\n\n public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);\n }\n\n public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);\n }\n\n public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'deal_value',\n 'label' => 'Deal Value',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_deal_value',\n 'max' => 'max_deal_value',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array\n {\n $value = [];\n $conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;\n $dialerOn && $value[] = self::CALL_TYPE_DIALER;\n\n return [\n 'id' => 'call_type',\n 'label' => 'Call Type',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => [\n self::CALL_TYPE_CONFERENCE,\n self::CALL_TYPE_DIALER,\n ],\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getMediaTypeFieldData(?AutomatedReport $report = null): array\n {\n $value = [];\n\n if ($report) {\n $value = $this->transformMediaTypes($report);\n }\n\n return [\n 'id' => 'media_types',\n 'label' => 'Export as',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => self::MEDIA_TYPE_OBJECTS,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'call_duration',\n 'label' => 'Call Duration',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_call_duration',\n 'max' => 'max_call_duration',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getRecipientsFieldData(?Team $team = null, array $value = []): array\n {\n return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);\n }\n\n public function getJiminnyRecipientsFieldData(array $value = []): array\n {\n return $this->recipientsService->getJiminnyRecipientsFieldData($value);\n }\n\n public function getAdditionalPromptInputFieldData(?string $value = null): array\n {\n return [\n 'id' => 'additional_prompt_input',\n 'label' => 'Special requirements',\n 'inputType' => InputTypeEnum::TEXTAREA,\n 'required' => false,\n 'placeholder' => 'What should be the focus of the report?',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCustomReportNameFieldData(?string $value = null): array\n {\n return [\n 'id' => 'custom_name',\n 'label' => 'Custom report name',\n 'inputType' => InputTypeEnum::TEXT,\n 'required' => false,\n 'placeholder' => 'Enter custom name',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n // data providers\n public function getTeams(?Partner $partner = null): array\n {\n $teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);\n\n $teamData = [];\n foreach ($teams as $team) {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n continue;\n }\n\n $teamData[] = $this->transformTeam($team);\n }\n\n return $teamData;\n }\n\n public function getTeamGroups(string $teamUuid): array\n {\n $data = [];\n $team = $this->getTeam($teamUuid);\n\n if ($team !== null) {\n $groups = $team->groups()->get();\n\n foreach ($groups as $group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array\n {\n $data = [];\n $teams = $this->getTeams(partner: $partner);\n\n foreach ($teams as $team) {\n if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {\n continue;\n }\n\n $data[] = [\n 'label' => $team['name'],\n 'groups' => $this->getTeamGroups($team['id']),\n ];\n }\n\n return $data;\n }\n\n public function getTeam(string $teamUuid): ?Team\n {\n return $this->teamRepository->idOrUuid($teamUuid);\n }\n\n public function getTeamById(int $teamId): ?Team\n {\n return $this->teamRepository->find($teamId);\n }\n\n public function getGroupsUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportGroups = $report->getGroups();\n foreach ($reportGroups as $groupId) {\n if ($group = $this->groupRepository->find($groupId)) {\n $uuids[] = $group->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getPlaybookCategoriesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $playbookCategories = $report->getPlaybookCategories();\n foreach ($playbookCategories as $id) {\n if ($category = $this->playbookCategoryRepository->find($id)) {\n $uuids[] = $category->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getDealAtCallStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getDealAtCallStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getCurrentDealStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getCurrentDealStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getRecipients());\n }\n\n public function getJiminnyUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function extractUserUuids(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => $user->getUuid())\n ->values()\n ->all();\n }\n\n // get mail data\n public function getRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getRecipients());\n }\n\n /**\n * @return array<UserContract>\n */\n public function getRecipientUserObjects(AutomatedReport $report): array\n {\n $userIds = $report->getRecipients()['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->values()\n ->all();\n }\n\n private function getJiminnyRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function buildRecipientUsers(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => [\n 'email' => $user->getEmailAddress(),\n 'name' => $user->getName(),\n 'timezone' => $user->getTimezone()->getName(),\n ])\n ->values()\n ->all();\n }\n\n public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array\n {\n if ($report->isAskJiminnyReport()) {\n $recipients = $this->resolveAskJiminnyRecipients($report);\n } else {\n $recipients = $this->getRecipientUsers($report);\n if ($includeJiminny) {\n $recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));\n }\n }\n\n $emails = [];\n\n return array_values(array_filter(\n $recipients,\n static function ($recipient) use (&$emails) {\n if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {\n return false;\n }\n $emails[] = $recipient['email'];\n\n return true;\n }\n ));\n }\n\n private function resolveAskJiminnyRecipients(AutomatedReport $report): array\n {\n $recipients = [];\n\n $creator = $report->getCreator();\n if ($creator !== null) {\n $recipients[] = [\n 'email' => $creator->getEmailAddress(),\n 'name' => $creator->getName(),\n 'timezone' => $creator->getTimezone()->getName(),\n ];\n }\n\n return array_merge(\n $recipients,\n $this->buildRecipientUsers($report->getRecipients()),\n $this->getGroupRecipientUsers($report),\n );\n }\n\n private function getGroupRecipientUsers(AutomatedReport $report): array\n {\n $users = [];\n foreach ($report->getGroups() as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group === null) {\n continue;\n }\n foreach ($group->getMembers() as $member) {\n $users[] = [\n 'email' => $member->getEmailAddress(),\n 'name' => $member->getName(),\n 'timezone' => $member->getTimezone()->getName(),\n ];\n }\n }\n\n return $users;\n }\n\n public function getReportTypeName(AutomatedReportResult $report): string\n {\n $type = $report->getReport()->getType();\n\n $getType = $this->transformReportType($type);\n\n return $getType['name'];\n }\n\n public function getReportPeriodName(AutomatedReportResult $report): string\n {\n $from = $report->getFromDate();\n $to = $report->getToDate();\n $frequency = $report->getReport()->getFrequency();\n\n if ($from === null || $to === null) {\n if (! $report->getReport()->isAskJiminnyReport()) {\n $invalidPeriod = $from === null ? 'from' : 'to';\n\n throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);\n }\n\n $timezone = $report->getReport()->getCreator()?->getTimezone();\n $period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);\n $from = $period['fromDate'];\n $to = $period['toDate'];\n }\n\n return $this->formatReportPeriodName($frequency, $from, $to);\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 getReportTeamsName(AutomatedReportResult $report): string\n {\n $groups = $report->getGroups();\n\n if (empty($groups)) {\n return 'All';\n }\n\n // Get group names from repository\n $groupNames = [];\n foreach ($groups as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group) {\n $groupNames[] = $group->getName();\n }\n }\n\n if (count($groupNames) === 1) {\n // Single team format\n $teamsName = $groupNames[0];\n } else {\n // Multiple teams format\n $teamsName = implode(', ', $groupNames);\n }\n\n return $teamsName;\n }\n\n public function getReportFileName(AutomatedReportResult $report): string\n {\n $customName = $report->getReport()->getCustomName();\n $periodName = $this->getReportPeriodName($report);\n $filenameSuffix = $this->getFilenameSuffix($report);\n\n if ($customName) {\n if ($filenameSuffix) {\n $customName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$customName} - {$periodName}\");\n }\n\n $baseName = $this->getReportTypeName($report);\n\n if ($filenameSuffix) {\n $baseName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}\");\n }\n\n public function getReportFileNameWithExtension(AutomatedReportResult $result): string\n {\n $extension = $this->getMediaTypeMetadata($result)['extension'];\n\n return $this->getReportFileName($result) . '.' . $extension;\n }\n\n public function sanitizeFileName(string $fileName): string\n {\n return str_replace(['/', '\\\\'], '-', $fileName);\n }\n\n public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool\n {\n $recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);\n if (in_array($user->getId(), $recipientIds, true)) {\n return true;\n }\n\n if ($report->isAskJiminnyReport()) {\n $groupId = $user->getGroupId();\n if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {\n return true;\n }\n }\n\n return false;\n }\n\n public function transformReportResults(Collection $automatedReportResults): array\n {\n $data = [];\n foreach ($automatedReportResults as $automatedReportResult) {\n /** @var AutomatedReportResult $automatedReportResult */\n\n $report = $automatedReportResult->getReport();\n\n $createdBy = $report->getCreator();\n $creator = [\n 'id' => $createdBy?->getUuid(),\n 'name' => $createdBy?->getName(),\n 'email' => $createdBy?->getEmailAddress(),\n 'photoUrl' => $createdBy?->getPhotoUrl(),\n ];\n\n $data[] = [\n 'id' => $automatedReportResult->getUuid(),\n 'name' => $automatedReportResult->getName(),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n 'recipients' => $this->buildRecipients($report),\n 'report_type' => $this->transformReportType($report->getType()),\n 'media_type' => $automatedReportResult->getMediaType(),\n 'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),\n 'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),\n 'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),\n 'creator' => $creator,\n ];\n }\n\n return $data;\n }\n\n private function buildRecipients(AutomatedReport $report): array\n {\n $creatorUuid = $report->getCreator()?->getUuid();\n\n $recipients = array_values(array_filter(\n $this->transformRecipients($report->getRecipients()),\n static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,\n ));\n\n if (! $report->isAskJiminnyReport()) {\n return $recipients;\n }\n\n return [\n ...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),\n ...$recipients,\n ];\n }\n\n public function hasCallTypeConference(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);\n }\n\n public function hasCallTypeDialer(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);\n }\n\n // transformers\n private function transformTeam(Team $team): array\n {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n return [];\n }\n\n return [\n 'id' => $team->getUuid(),\n 'name' => $team->getName(),\n ];\n }\n\n private function transformReportFullView(AutomatedReport $report): array\n {\n $base = $this->transformReportBase($report);\n\n return $report->getType() === self::TYPE_ASK_JIMINNY\n ? $base + $this->transformAskJiminnyFields($report)\n : $base + $this->transformStandardReportFields($report);\n }\n\n private function transformReportBase(AutomatedReport $report): array\n {\n return [\n 'id' => $report->getUuid(),\n 'organization' => $this->transformOrganization(team: $report->getTeam()),\n 'report_type' => $this->transformReportType($report->getType()),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n ];\n }\n\n private function transformStandardReportFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n\n return [\n 'report_enabled' => $report->getStatus(),\n 'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),\n 'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),\n 'deal_value_min' => $report->getDealValueMin(),\n 'deal_value_max' => $report->getDealValueMax(),\n 'call_types' => $this->transformCallType($report->getCallTypes()),\n 'media_types' => $this->transformMediaTypes($report),\n 'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),\n 'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),\n 'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),\n 'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),\n 'recipients' => $this->transformRecipients($report->getRecipients()),\n 'created_by' => $this->transformCreator($report->getCreator()),\n 'additional_prompt_input' => $report->getAdditionalPromptInput(),\n 'custom_name' => $report->getCustomName(),\n 'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),\n 'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),\n 'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),\n ];\n }\n\n private function transformAskJiminnyFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n $creatorId = $report->getAttribute('created_by');\n $explicitUserIds = array_values(array_filter(\n $report->getRecipients()['users'] ?? [],\n static fn ($id) => $id !== $creatorId\n ));\n\n return [\n 'report_name' => $report->getCustomName(),\n 'enabled' => $report->getStatus(),\n 'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),\n 'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),\n 'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),\n 'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),\n ];\n }\n\n private function transformOrganization(?Team $team): array\n {\n return [\n 'id' => $team?->getUuid(),\n 'name' => $team?->getName(),\n ];\n }\n\n private function transformReportType(string $type): array\n {\n foreach (self::ALL_TYPES as $typeItem) {\n if ($typeItem['id'] === $type) {\n return $typeItem;\n }\n }\n\n return [];\n }\n\n private function transformCallType(array $types): array\n {\n $result = [];\n $callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];\n\n foreach ($types as $type) {\n foreach ($callTypes as $callTypeItem) {\n if ($callTypeItem['id'] === $type) {\n $result[] = $callTypeItem;\n\n break;\n }\n }\n }\n\n return $result;\n }\n\n private function transformMediaTypes(AutomatedReport $report): array\n {\n $values = [];\n\n foreach ($report->getMediaTypes() as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n continue;\n }\n\n $values[] = match ($mediaType) {\n self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,\n self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,\n };\n }\n\n return $values;\n }\n\n private function transformFrequency(string $frequency): array\n {\n foreach (self::ALL_FREQUENCIES as $frequencyItem) {\n if ($frequencyItem['id'] === $frequency) {\n return $frequencyItem;\n }\n }\n\n return [];\n }\n\n public function transformDurationToMinutes(?int $duration): ?int\n {\n if (! $duration) {\n return null;\n }\n\n return (int) ($duration / 60);\n }\n\n private function transformGroups(?Team $team, array $groupsIds): array\n {\n if (empty($groupsIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($groupsIds as $groupId) {\n $group = $team->groups()->where('id', $groupId)->first();\n\n if ($group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n 'photoUrl' => $group->getPhotoUrl(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformStages(?Team $team, array $stagesIds): array\n {\n if (empty($stagesIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($stagesIds as $stageId) {\n $stage = $team->stages()->where('id', $stageId)->first();\n\n if ($stage) {\n $data[] = [\n 'id' => $stage->getUuid(),\n 'name' => $stage->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformRecipients(array $recipients): array\n {\n $users = [];\n foreach ($recipients['users'] ?? [] as $userId) {\n $users[] = $this->transformUser($userId);\n }\n\n return $users;\n }\n\n private function transformCreator(?User $user): ?array\n {\n if ($user === null) {\n return null;\n }\n\n return $this->transformUser($user->getId());\n }\n\n private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array\n {\n if ($prompt === null) {\n return null;\n }\n\n return [\n 'id' => $prompt->getUuid(),\n 'name' => $prompt->getTitle(),\n ];\n }\n\n private function transformSafeSearch(?Search $search): ?array\n {\n if ($search === null) {\n return null;\n }\n\n return [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ];\n }\n\n private function transformUser(int $userId): array\n {\n /* @var ?User $user */\n $user = $this->userRepository->find($userId);\n\n return [\n 'id' => $user?->getUuid(),\n 'name' => $user?->getName(),\n 'email' => $user?->getEmailAddress(),\n 'photoUrl' => $user?->getPhotoUrl(),\n ];\n }\n\n public function create(array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $validatedData['created_by'] = auth()->id();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function update(string $uuid, array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $report = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $report) {\n throw new InvalidArgumentException('Report not found');\n }\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Create an Ask Jiminny report.\n */\n public function createAskJiminnyReport(array $data, User $creator): array\n {\n $validatedData = $this->validateAskJiminnyReportData($data, $creator);\n $validatedData['created_by'] = $creator->getId();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Update an Ask Jiminny report.\n */\n public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array\n {\n if (! $report->isAskJiminnyReport()) {\n throw new InvalidArgumentException('Report is not an Ask Jiminny report');\n }\n\n $validatedData = $this->validateAskJiminnyReportData($data, $user);\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array\n {\n if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {\n throw new InvalidArgumentException(\n 'This report is missing a saved search or prompt. ' .\n 'Edit the report to complete the setup before enabling it.'\n );\n }\n\n $this->automatedReportsRepository->update($report, ['status' => $status]);\n\n return $this->transformReportFullView($report->fresh());\n }\n\n /**\n * Validate and transform data for Ask Jiminny reports.\n */\n private function validateAskJiminnyReportData(array $data, User $user): array\n {\n // Validate name\n $name = trim($data['report_name'] ?? '');\n if (empty($name)) {\n throw new InvalidArgumentException('Report name is required');\n }\n if (mb_strlen($name) > 50) {\n throw new InvalidArgumentException('Report name must be 50 characters or less');\n }\n\n // Validate frequency (only daily, weekly, monthly for Ask Jiminny)\n $frequency = $data['frequency'] ?? null;\n $askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];\n if (! in_array($frequency, $askJiminnyFrequencies, true)) {\n throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');\n }\n\n // Validate expiration date\n $expiresAt = $data['expires_on'] ?? null;\n if (empty($expiresAt)) {\n throw new InvalidArgumentException('Expiration date is required');\n }\n\n try {\n $expiresAtDate = Carbon::parse($expiresAt);\n } catch (InvalidFormatException $e) {\n throw new InvalidArgumentException('Expiration date format is invalid');\n }\n $maxExpiration = Carbon::now()->addYear()->endOfDay();\n if ($expiresAtDate->gt($maxExpiration)) {\n throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');\n }\n if ($expiresAtDate->isPast()) {\n throw new InvalidArgumentException('Expiration date cannot be in the past');\n }\n\n // Validate saved search\n $activitySearchId = $data['saved_search'] ?? null;\n if (empty($activitySearchId)) {\n throw new InvalidArgumentException('Saved search is required');\n }\n $savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);\n if (! $savedSearch) {\n throw new InvalidArgumentException('Saved search not found or does not belong to you');\n }\n\n // Validate saved prompt\n $askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;\n if (empty($askAnythingPromptId)) {\n throw new InvalidArgumentException('Ask Jiminny prompt is required');\n }\n $prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);\n if (! $prompt) {\n throw new InvalidArgumentException('Ask Jiminny prompt not found');\n }\n\n // Validate status\n $status = $data['enabled'] ?? false;\n\n $recipientUserIds = [$user->getId()];\n\n if (! empty($data['share_users'])) {\n $sharedUserIds = $this->validateAndGetUserIdsByTeam(\n $user->team,\n (array) $data['share_users']\n );\n $recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);\n }\n\n $sharedGroupIds = [];\n if (! empty($data['share_teams'])) {\n $sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);\n }\n\n $recipientUserIds = array_values(array_unique($recipientUserIds));\n\n return [\n 'team_id' => $user->getTeamId(),\n 'type' => self::TYPE_ASK_JIMINNY,\n 'status' => (bool) $status,\n 'frequency' => $frequency,\n 'custom_name' => $name,\n 'activity_search_id' => $savedSearch->getId(),\n 'ask_anything_prompt_id' => $prompt->getId(),\n 'expires_at' => $expiresAtDate->toDateString(),\n 'media_types' => [self::MEDIA_TYPE_PDF],\n 'call_types' => [],\n 'recipients' => ['users' => $recipientUserIds],\n 'groups' => $sharedGroupIds,\n ];\n }\n\n public static function getAskJiminnyFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::ASK_JIMINNY_FREQUENCIES);\n }\n\n public function getAskJiminnyReportFilters(User $user): array\n {\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n return [\n [\n 'id' => 'prompt',\n 'label' => 'Prompt',\n 'options' => $prompts,\n ],\n [\n 'id' => 'saved_search',\n 'label' => 'Saved Search',\n 'options' => $savedSearches,\n ],\n ];\n }\n\n public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array\n {\n $team = $user->getTeam();\n $userTimezone = $user->getTimezone();\n\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n $teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ])->values()->all();\n\n $shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];\n\n $sharedTeamsValue = [];\n $sharedUsersValue = [];\n if ($report) {\n $sharedTeamsValue = $this->transformGroups($team, $report->getGroups());\n\n $recipientUserIds = $report->getRecipients()['users'] ?? [];\n $creatorId = $report->getAttribute('created_by');\n $sharedUserIds = array_values(array_filter(\n $recipientUserIds,\n static fn ($id) => $id !== $creatorId\n ));\n $sharedUsersValue = collect($sharedUserIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (User $u) => [\n 'id' => $u->getUuid(),\n 'name' => $u->getName(),\n ])\n ->values()\n ->all();\n }\n\n return [\n 'fields' => [\n [\n 'id' => 'enabled',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'label' => '',\n 'value' => $report?->getStatus() ?? false,\n ],\n [\n 'id' => 'report_name',\n 'inputType' => InputTypeEnum::TEXT,\n 'label' => 'Name',\n 'placeholder' => 'Enter name',\n 'required' => true,\n 'validation' => ['maxLength' => 50],\n 'value' => $report?->getCustomName() ?? '',\n ],\n [\n 'id' => 'frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Frequency',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::ASK_JIMINNY_FREQUENCIES,\n 'value' => $report ? $this->transformFrequency($report->getFrequency()) : null,\n ],\n [\n 'id' => 'expires_on',\n 'inputType' => InputTypeEnum::DATE,\n 'label' => 'Expires on',\n 'required' => true,\n 'placeholder' => 'Select',\n 'validation' => [\n 'minDate' => now($userTimezone)->toDateString(),\n 'maxDate' => now($userTimezone)->addYear()->toDateString(),\n ],\n 'value' => $report?->getExpiresAt()?->toDateString(),\n ],\n [\n 'id' => 'share_teams',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team',\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $teamGroups,\n 'value' => $sharedTeamsValue,\n ],\n [\n 'id' => 'share_users',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team member',\n 'required' => false,\n 'placeholder' => 'Select',\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'options' => $shareUsers,\n 'value' => $sharedUsersValue,\n ],\n [\n 'id' => 'saved_search',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Saved search',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $savedSearches,\n 'value' => $report && $report->getSavedSearch() ? [\n 'id' => $report->getSavedSearch()->getUuid(),\n 'name' => $report->getSavedSearch()->getName(),\n ] : null,\n ],\n [\n 'id' => 'ask_jiminny_prompt',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Ask Jiminny prompt',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $prompts,\n 'value' => $report && $report->getAskAnythingPrompt() ? [\n 'id' => $report->getAskAnythingPrompt()->getUuid(),\n 'name' => $report->getAskAnythingPrompt()->getTitle(),\n ] : null,\n ],\n ],\n ];\n }\n\n private function updateResultNames(AutomatedReport $automatedReport): void\n {\n $results = $this->automatedReportsRepository->getResultsByReport($automatedReport);\n\n foreach ($results as $result) {\n $result->update(['name' => $this->getReportFileName($result)]);\n }\n }\n\n public function updateStatus(string $uuid, array $data): array\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $automatedReport->update([\n 'status' => $status,\n ]);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n private function generateOneOffReport(AutomatedReport $automatedReport): void\n {\n // the scheduler handles all the other frequency types\n if ($automatedReport->getStatus() === false || $automatedReport->getFrequency() !== self::FREQUENCY_ONE_OFF) {\n return;\n }\n\n $this->dispatcher->dispatch(new RequestGenerateReportJob($automatedReport->getUuid()));\n }\n\n public function getReport(string $uuid, ?Partner $partner = null): AutomatedReport\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n if ($partner !== null && ! $partner->isDefaultPartner() && $automatedReport->team->partner_id !== $partner->getId()) {\n throw new ModelNotFoundException('Report not found');\n }\n\n return $automatedReport;\n }\n\n public function get(string $uuid, ?Partner $partner = null): array\n {\n $automatedReport = $this->getReport($uuid, $partner);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function list(string $sortColumn = 'created_at', string $sortDirection = 'desc', ?Partner $partner = null): array\n {\n $results = [];\n $collection = $this->automatedReportsRepository->getAllStandardReports($sortColumn, $sortDirection, $partner);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function listAskJiminnyReports(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): array {\n $results = [];\n $collection = $this->automatedReportsRepository->getAskJiminnyReportsByUser($user, $sortColumn, $sortDirection);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function delete(string $uuid): void\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $automatedReport->delete();\n }\n\n public function createReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n return $this->automatedReportsRepository->createResult(\n array_merge(\n [\n 'report_id' => $automatedReport->getId(),\n 'status' => AutomatedReportResult::STATUS_DEFAULT,\n ],\n $data\n )\n );\n }\n\n public function getOrCreateReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n $existing = $this->automatedReportsRepository->findLatestSameDayDefaultOrFailedResult($automatedReport);\n\n if ($existing !== null) {\n $existing->update(['status' => AutomatedReportResult::STATUS_DEFAULT]);\n\n return $existing;\n }\n\n return $this->createReportResult($automatedReport, $data);\n }\n\n public function getReportResult(string $resultUuid): AutomatedReportResult\n {\n $report = $this->automatedReportsRepository->findResultByUuid($resultUuid);\n\n if (! $report) {\n throw new ModelNotFoundException('Report Result not found');\n }\n\n return $report;\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return $this->automatedReportsRepository->findChildResult($result, $type);\n }\n\n // prophet API calls\n /**\n * @throws ApplicationException\n */\n public function getGenerateReportPayload(AutomatedReport $automatedReport, string $reportResultUuid): array\n {\n $period = $this->calculateFromAndToDate($automatedReport);\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n return [\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResultUuid,\n 'report_type' => $automatedReport->getType(),\n 'media_types' => $automatedReport->getMediaTypes(),\n 'from_date' => $fromDate->startOfDay()->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->endOfDay()->format(DateTimeInterface::RFC3339),\n 'group_ids' => $automatedReport->getGroups(),\n 'call_deal_stage' => $automatedReport->getDealAtCallStages(),\n 'current_deal_stage' => $automatedReport->getCurrentDealStages(),\n 'deal_min_value' => $automatedReport->getDealValueMin(),\n 'deal_max_value' => $automatedReport->getDealValueMax(),\n 'call_types' => $automatedReport->getCallTypes(),\n 'call_duration_min_seconds' => $automatedReport->getCallDurationMin(),\n 'call_duration_max_seconds' => $automatedReport->getCallDurationMax(),\n 'special_requirements' => $automatedReport->getAdditionalPromptInput(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->formatReportPeriodName(\n $automatedReport->getFrequency(),\n $fromDate,\n $toDate,\n ),\n 'playbook_categories' => $automatedReport->getPlaybookCategories(),\n 'custom_name' => $automatedReport->getCustomName(),\n ];\n }\n\n // $inputPayload - FE payload structure\n public function getActivitiesCountPayload(array $inputPayload): array\n {\n // Use validateAndTransformData to validate and normalize input\n $validatedData = $this->validateAndTransformData($inputPayload);\n $period = $this->calculateFromAndToDatePeriod(\n $validatedData['frequency'],\n Carbon::parse($validatedData['from']),\n Carbon::parse($validatedData['to']),\n );\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n // Create payload similar to getGenerateReportPayload\n return [\n 'team_id' => $validatedData['team_id'],\n 'group_ids' => $validatedData['groups'] ?? [],\n 'report_type' => $validatedData['type'],\n 'from_date' => $fromDate->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->format(DateTimeInterface::RFC3339),\n 'call_deal_stage' => $validatedData['deal_at_call_stages'] ?? [],\n 'current_deal_stage' => $validatedData['current_deal_stages'] ?? [],\n 'deal_min_value' => $validatedData['deal_value_min'] ?? null,\n 'deal_max_value' => $validatedData['deal_value_max'] ?? null,\n 'call_types' => $validatedData['call_types'],\n 'call_duration_min_seconds' => $validatedData['call_duration_min'] ?? null,\n 'call_duration_max_seconds' => $validatedData['call_duration_max'] ?? null,\n 'special_requirements' => $validatedData['additional_prompt_input'] ?? null,\n 'playbook_categories' => $validatedData['playbook_categories'] ?? [],\n 'request_id' => null,\n 'callback_url' => null,\n ];\n }\n\n public function shouldSendReport(array $users, ?CarbonInterface $generatedAt = null): bool\n {\n if (empty($users)) {\n return false;\n }\n\n $earliestTz = collect($users)\n ->mapWithKeys(function (array $user) {\n $tz = new DateTimeZone($user['timezone']);\n $nowUtc = new DateTime('now', new DateTimeZone('UTC'));\n $offset = $tz->getOffset($nowUtc);\n\n return [$user['timezone'] => $offset];\n })\n ->sortDesc()\n ->keys()\n ->first();\n\n $now = Carbon::now($earliestTz);\n $isScheduledTime = (int) $now->format('H') === self::SENT_REPORT_AT_HOURS;\n\n if ($isScheduledTime) {\n return true;\n }\n\n return $this->hasPassedScheduledTime($generatedAt, $earliestTz);\n }\n\n public function hasPassedScheduledTime(?CarbonInterface $generatedAt, string $timezone): bool\n {\n if ($generatedAt === null) {\n return false;\n }\n\n $now = Carbon::now($timezone);\n $scheduledTime = $now->copy()->setTime(self::SENT_REPORT_AT_HOURS, 0, 0);\n\n if ($now->hour < self::SENT_REPORT_AT_HOURS) {\n $scheduledTime = $scheduledTime->subDay();\n }\n\n $scheduledTimeUtc = $scheduledTime->copy()->utc();\n $generatedAtUtc = $generatedAt->copy()->utc();\n $nowUtc = $now->copy()->utc();\n\n return $generatedAtUtc->lt($scheduledTimeUtc) && $nowUtc->gt($scheduledTimeUtc);\n }\n\n public function calculateFromAndToDatePeriod(\n string $frequency,\n ?Carbon $fromDate = null,\n ?Carbon $toDate = null,\n DateTimeZone|string|null $timezone = 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($timezone);\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()->subWeek()->startOfWeek(CarbonInterface::MONDAY),\n 'toDate' => $now->copy()->subWeek()->endOfWeek(CarbonInterface::SUNDAY),\n ],\n self::FREQUENCY_MONTHLY => [\n 'fromDate' => $now->copy()->subMonthNoOverflow()->startOfMonth(),\n 'toDate' => $now->copy()->subMonthNoOverflow()->endOfMonth(),\n ],\n self::FREQUENCY_QUARTERLY => [\n 'fromDate' => $now->copy()->subQuarterNoOverflow()->startOfQuarter(),\n 'toDate' => $now->copy()->subQuarterNoOverflow()->endOfQuarter(),\n ],\n default => throw new InvalidArgumentException(\"Unsupported frequency: {$frequency}\"),\n };\n }\n\n private function calculateFromAndToDate(AutomatedReport $automatedReport): array\n {\n return $this->calculateFromAndToDatePeriod(\n $automatedReport->getFrequency(),\n $automatedReport->getFrom(),\n $automatedReport->getTo()\n );\n }\n\n public function getAskJiminnyGenerateReportPayload(\n AutomatedReport $automatedReport,\n AutomatedReportResult $reportResult,\n array $activityIds,\n ): array {\n return [\n 'user_question' => $automatedReport->getAskAnythingPrompt()?->getContent(),\n 'call_ids' => array_map('strval', $activityIds),\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResult->getUuid(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->getReportPeriodName($reportResult),\n 'report_name' => $automatedReport->getCustomName(),\n ];\n }\n\n private function getCallbackUrl(): string\n {\n return $this->webhookService->route('jiminny.webhook.reports.ready');\n }\n\n /**\n * Validate and transform payload data for automated reports\n *\n * @param array $data\n *\n * @throws InvalidArgumentException\n *\n * @return array\n */\n private function validateAndTransformData(array $data): array\n {\n // Validate organization (team) and check feature\n $team = $this->validateOrganization($data['organization'] ?? null);\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $type = $this->validateReportType($data['report_type'] ?? null);\n $frequency = $this->validateFrequency($data['frequency'] ?? null);\n $additionalPromptInput = $this->validateAdditionalPromptInput(\n $data['additional_prompt_input'] ?? null\n );\n $customReportName = $this->validateCustomReportName($data['custom_name'] ?? null);\n\n // Prepare data for the database\n $reportData = [\n 'team_id' => $team->getId(),\n 'type' => $type,\n 'status' => $status,\n 'frequency' => $frequency,\n 'additional_prompt_input' => $additionalPromptInput,\n 'custom_name' => $customReportName,\n ];\n\n // Validate deal values\n $reportData = $this->validateDealValues($data, $reportData);\n\n // Validate date range\n $reportData = $this->validateDateRange($data, $reportData, $frequency);\n\n // Validate call durations\n $reportData = $this->validateCallDurations($data, $reportData);\n\n // Validate call types\n $reportData = $this->validateCallTypes($data, $reportData);\n\n // Validate media types\n $reportData = $this->validateMediaTypes($data, $reportData);\n\n // Validate groups\n if (isset($data['teams'])) {\n $reportData['groups'] = $this->validateAndGetGroupIds($team, $data['teams']);\n }\n\n // Validate deal stages\n $reportData = $this->validateDealStages($data, $reportData, $team, $type);\n\n // Validate playbook categories\n $reportData = $this->validatePlaybookCategories($data, $reportData, $team);\n\n // Validate recipients\n $reportData['recipients'] = [\n 'users' => $this->validateAndGetUserIdsByTeam($team, $data['recipients'] ?? []),\n ];\n\n if (isset($data['jiminny_recipients'])) {\n // Validate Jiminny recipients\n $reportData['jiminny_recipients'] = [\n 'users' => $this->validateAndGetJiminnyUserIds((array) $data['jiminny_recipients']),\n ];\n }\n\n return $reportData;\n }\n\n private function validateDealValues(array $data, array $reportData): array\n {\n if (isset($data['min_deal_value'])) {\n $reportData['deal_value_min'] = (int) $data['min_deal_value'];\n\n if ($reportData['deal_value_min'] > 4294967295 || $reportData['deal_value_min'] < 0) {\n throw new InvalidArgumentException('Min deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_deal_value'])) {\n $reportData['deal_value_max'] = (int) $data['max_deal_value'];\n\n if ($reportData['deal_value_max'] > 4294967295 || $reportData['deal_value_max'] < 0) {\n throw new InvalidArgumentException('Max deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['min_deal_value'], $data['max_deal_value'])\n && $data['min_deal_value'] > $data['max_deal_value']\n ) {\n throw new InvalidArgumentException('Min deal value cannot be greater than max deal value');\n }\n\n return $reportData;\n }\n\n private function validateDateRange(array $data, array $reportData, string $frequency): array\n {\n // Set date range only for one_off frequency\n if ($frequency === 'one_off') {\n if (isset($data['start_date_period'])) {\n $reportData['from'] = $this->parseDate($data['start_date_period']);\n }\n\n if (isset($data['end_date_period'])) {\n $reportData['to'] = $this->parseDate($data['end_date_period']);\n }\n\n if (empty($reportData['from']) || empty($reportData['to'])) {\n throw new InvalidArgumentException(\n 'Start date and end date are required for one_off frequency'\n );\n }\n } else {\n $reportData['from'] = null;\n $reportData['to'] = null;\n }\n\n return $reportData;\n }\n\n private function validateCallDurations(array $data, array $reportData): array\n {\n // Convert call durations from minutes to seconds\n if (isset($data['min_call_duration'])) {\n $reportData['call_duration_min'] = (int) $data['min_call_duration'] * 60;\n\n if ($reportData['call_duration_min'] > 4294967295 || $reportData['call_duration_min'] < 0) {\n throw new InvalidArgumentException('Min call duration should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_call_duration'])) {\n $reportData['call_duration_max'] = (int) $data['max_call_duration'] * 60;\n\n if ($reportData['call_duration_max'] > 4294967295 || $reportData['call_duration_max'] < 0) {\n throw new InvalidArgumentException('Max call duration should be between 0 and 4294967295');\n }\n }\n\n return $reportData;\n }\n\n private function validateCallTypes(array $data, array $reportData): array\n {\n // Set call types\n $reportData['call_types'] = $data['call_type'] ?? [];\n if (empty($reportData['call_types'])) {\n $reportData['call_types'] = self::getCallTypes();\n }\n\n foreach ($reportData['call_types'] as $callType) {\n if (! in_array($callType, self::getCallTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Call type %s is invalid', $callType));\n }\n }\n\n return $reportData;\n }\n\n private function validateMediaTypes(array $data, array $reportData): array\n {\n // Set media types from input data\n $reportData['media_types'] = $data['media_types'] ?? [];\n\n if (empty($reportData['media_types'])) {\n throw new InvalidArgumentException('Media types are required');\n }\n\n foreach ($reportData['media_types'] as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n throw new InvalidArgumentException(sprintf('Media type %s is invalid', $mediaType));\n }\n }\n\n return $reportData;\n }\n\n private function validateDealStages(array $data, array $reportData, Team $team, string $reportType): array\n {\n // Validate and set deal stages\n if (isset($data['deal_stage_at_call'])) {\n $reportData['deal_at_call_stages'] =\n $this->validateAndGetDealStageIds($team, $data['deal_stage_at_call'], 'Deal stage at call');\n }\n\n if (isset($data['current_deal_stage'])) {\n $reportData['current_deal_stages'] =\n $this->validateAndGetDealStageIds($team, $data['current_deal_stage'], 'Current deal stage');\n }\n\n // Ensure current_deal_stage is not provided for loss_analysis report type\n if ($reportType === self::TYPE_LOSS_ANALYSIS && ! empty($data['current_deal_stage'])) {\n throw new InvalidArgumentException('Current deal stage is not applicable for Loss Analysis reports');\n }\n\n return $reportData;\n }\n\n // transform uuid to id\n private function validatePlaybookCategories(array $data, array $reportData, Team $team): array\n {\n $key = 'playbook_categories';\n\n if (isset($data[$key])) {\n $payloadIds = $data[$key];\n $ids = [];\n\n foreach ($payloadIds as $uuid) {\n $uuid = (string) $uuid;\n\n try {\n $playbookCategory = $this->playbookCategoryRepository->findByUuid($uuid);\n } catch (Throwable $throwable) {\n Log::error(__METHOD__ . ' ' . $throwable->getMessage());\n\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory) {\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory->hasPlaybook()) {\n throw new InvalidArgumentException(sprintf('Playbook category %s has no playbook', $uuid));\n }\n\n if ($playbookCategory->getPlaybook()->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Playbook category %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $ids[] = $playbookCategory->getId();\n }\n\n $reportData[$key] = $ids;\n }\n\n return $reportData;\n }\n\n private function validateReportStatus($status): bool\n {\n if (! in_array($status, [true, false], true)) {\n throw new InvalidArgumentException('Report status is invalid');\n }\n\n return $status;\n }\n\n private function validateReportType($type): string\n {\n if (! in_array($type, self::getTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Report type is invalid: %s', $type));\n }\n\n return $type;\n }\n\n private function validateFrequency($frequency): string\n {\n if (! in_array($frequency, self::getFrequencies(), true)) {\n throw new InvalidArgumentException('Frequency is invalid');\n }\n\n return $frequency;\n }\n\n private function validateAdditionalPromptInput(?string $additionalPromptInput): ?string\n {\n if ($additionalPromptInput && strlen($additionalPromptInput) > 5000) {\n throw new InvalidArgumentException('Additional Prompt Input should be less than 5000 characters');\n }\n\n return $additionalPromptInput;\n }\n\n private function validateCustomReportName(?string $customReportName): ?string\n {\n if ($customReportName === null || $customReportName === '') {\n return null;\n }\n\n if (strlen($customReportName) > 70) {\n throw new InvalidArgumentException('Custom report name should be less than 70 characters');\n }\n\n return $customReportName;\n }\n\n private function validateOrganization(?string $organizationUuid): Team\n {\n if (! $organizationUuid) {\n throw new InvalidArgumentException('Organization is required');\n }\n\n $team = $this->teamRepository->idOrUuid($organizationUuid);\n\n if (! $team) {\n throw new InvalidArgumentException('Organization not found');\n }\n\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n throw new InvalidArgumentException('Organization does not have the Automated Reports feature');\n }\n\n return $team;\n }\n\n private function validateAndGetGroupIds(Team $team, array $teamUuids): array\n {\n $groupIds = [];\n\n foreach ($teamUuids as $uuid) {\n $group = $this->groupRepository->findByUuid($uuid);\n\n if ($group === null || $group->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Group %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $groupIds[] = $group->getId();\n\n }\n\n return $groupIds;\n }\n\n private function validateAndGetDealStageIds(Team $team, array $stageUuids, string $propertyLabel): array\n {\n $stageIds = [];\n\n foreach ($stageUuids as $uuid) {\n $stage = $this->stageRepository->findByUuid($uuid);\n\n if ($stage === null || $stage->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Stage %s not found for team %s for %s', $uuid, $team->getUuid(), $propertyLabel)\n );\n }\n\n $stageIds[] = $stage->getId();\n }\n\n return $stageIds;\n }\n\n private function validateAndGetUserIds(array $userUuids, callable $teamCheck): array\n {\n if (empty($userUuids)) {\n return [];\n }\n\n $userIds = [];\n\n foreach ($userUuids as $uuid) {\n $user = $this->userRepository->findByUuid($uuid);\n\n if (! $user || ! $user->isStatusActive()) {\n throw new InvalidArgumentException(\n sprintf('User %s not found or is not active', $uuid)\n );\n }\n\n if (! $teamCheck($user)) {\n throw new InvalidArgumentException(\n sprintf('User %s does not belong to the allowed team(s)', $uuid)\n );\n }\n\n $userIds[] = $user->getId();\n }\n\n return $userIds;\n }\n\n private function validateAndGetUserIdsByTeam(Team $team, array $userUuids): array\n {\n return $this->validateAndGetUserIds($userUuids, fn ($user) => $user->getTeamId() === $team->getId());\n }\n\n private function validateAndGetJiminnyUserIds(array $userUuids): array\n {\n $allowedTeamIds = config('kiosk.teamIds', []);\n\n return $this->validateAndGetUserIds($userUuids, fn ($user) => in_array($user->getTeamId(), $allowedTeamIds, true));\n }\n\n private function parseDate(string $dateString): string\n {\n return date('Y-m-d H:i:s', strtotime($dateString));\n }\n\n private function generateReportResultViewUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.view', ['uuid' => $result->getUuid()]);\n }\n\n private function generateReportResultDownloadUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.download', ['uuid' => $result->getUuid()]);\n }\n\n private function getReportMediaRouteResource(AutomatedReportResult $result): string\n {\n if ($result->getMediaType() === self::MEDIA_TYPE_PDF) {\n return self::PDF_KEY;\n } elseif ($result->getMediaType() === self::MEDIA_TYPE_PODCAST) {\n return self::AUDIO_KEY;\n }\n\n throw new \\InvalidArgumentException('Unknown media type.');\n }\n\n public function getMediaPath(AutomatedReportResult $result): ?string\n {\n $url = match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => $result->getPdfUrl(),\n self::MEDIA_TYPE_PODCAST => $result->getPodcastAudioUrl(),\n default => null,\n };\n\n if ($url === null) {\n return null;\n }\n\n $path = parse_url(trim($url, '\"\\''), PHP_URL_PATH);\n\n return $path ?: null;\n }\n\n public function getFilenameSuffix(AutomatedReportResult $result): ?string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => 'Podcast',\n default => null,\n };\n }\n\n public function getMailSubjectSuffix(AutomatedReportResult $result): string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => 'report',\n self::MEDIA_TYPE_PODCAST => 'podcast',\n default => '',\n };\n }\n\n public function getMediaTypeMetadata(AutomatedReportResult $result): array\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => ['extension' => 'mp3', 'mime' => 'audio/mpeg'],\n self::MEDIA_TYPE_PDF => ['extension' => 'pdf', 'mime' => 'application/pdf'],\n default => ['extension' => null, 'mime' => null],\n };\n }\n\n public function deleteS3Files(AutomatedReportResult $result): void\n {\n $teamUuid = $result->getReport()->getTeam()->getUuid();\n $reportUuid = $result->getUuid();\n\n // delete all files for a report uuid no mather of pdf, podcast, or both\n // in case of both - the podcast files are linked to the pdf (parent) uuid\n // pdf and podcast date times should be close\n $path = sprintf('%s/%s/%s', $teamUuid, self::S3_DIR, $reportUuid);\n\n foreach (self::FILE_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted S3 file', [\n 'path' => $file,\n ]);\n }\n }\n\n foreach (self::FILE_PODCAST_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '_podcast.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted Podcast S3 file', [\n 'path' => $file,\n ]);\n }\n }\n }\n\n /**\n *\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): Collection\n {\n return $this->automatedReportsRepository->getTeamIdsWithReportsResults($teamId);\n }\n\n /**\n * Core delete logic for report results using a query\n *\n * @param Builder $query\n * @param array $logContext\n *\n * @return int\n */\n private function deleteReportResultsByQuery(Builder $query, array $logContext = []): int\n {\n $deletedCount = 0;\n\n if ($query->exists()) {\n Log::info(\n 'Run delete report results',\n array_merge(\n $logContext,\n [\n 'service' => 'AutomatedReportsService',\n ]\n )\n );\n\n $query->chunkById(50, function ($results) use (&$deletedCount, $logContext) {\n foreach ($results as $result) {\n $this->deleteReportResult($result);\n $deletedCount++;\n\n Log::info(\n 'Deleted a report result',\n array_merge(\n $logContext,\n [\n 'result_id' => $result->getId(),\n 'report_id' => $result->getReportId(),\n ]\n )\n );\n }\n });\n }\n\n return $deletedCount;\n }\n\n /**\n * Delete report results for a team by retention period\n *\n * @param Team $team\n * @param CarbonImmutable $retentionDate\n *\n * @return int Number of deleted report results\n */\n public function deleteReportsResultsInRetentionPeriod(Team $team, CarbonImmutable $retentionDate): int\n {\n $reportIds = $this->automatedReportsRepository->getReportIdsByTeam($team);\n\n if ($reportIds->isEmpty()) {\n return 0;\n }\n\n $query = $this->automatedReportsRepository\n ->getReportResultsQueryForRetention($team, $retentionDate);\n\n return $this->deleteReportResultsByQuery($query, [\n 'team_id' => $team->getId(),\n 'retention_date' => $retentionDate->toDateTimeString(),\n ]);\n }\n\n /**\n * Delete ALL report results for a specific automated report\n *\n * @param string $uuid\n *\n * @return int\n */\n public function deleteReportResults(string $uuid): int\n {\n $report = $this->getReport($uuid);\n\n $query = $this->automatedReportsRepository->getResultsByReportQuery($report);\n\n return $this->deleteReportResultsByQuery($query, [\n 'report_uuid' => $uuid,\n 'report_id' => $report->getId(),\n ]);\n }\n\n public function deleteReportResult(AutomatedReportResult $result): void\n {\n $this->deleteS3Files($result);\n\n $result->delete();\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getTeamReports(Team $team): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getReportsByTeam($team);\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getReportResults(AutomatedReport $report): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getResultsByReport($report);\n }\n\n public function deleteAllReportResults(AutomatedReport $report): void\n {\n $results = $this->getReportResults($report);\n\n /** @var AutomatedReportResult $result */\n foreach ($results as $result) {\n Log::info('Deleting result', [\n 'report' => $report->getId(),\n 'result' => $result->getId(),\n ]);\n\n $this->deleteReportResult($result);\n }\n }\n\n public function deleteAllData(Team $team): void\n {\n Log::info('Deleting automated report and results for team', [\n 'team' => $team->getId(),\n ]);\n\n $reports = $this->getTeamReports($team);\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n Log::info('Deleting report', [\n 'team' => $team->getId(),\n 'report' => $report->getId(),\n ]);\n\n $this->deleteAllReportResults($report);\n\n $report->delete();\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Carbon\\CarbonImmutable;\nuse Carbon\\CarbonInterface;\nuse Carbon\\Exceptions\\InvalidFormatException;\nuse DateTime;\nuse DateTimeInterface;\nuse DateTimeZone;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\InputTypeEnum;\nuse Jiminny\\Component\\AskAnything\\AskAnythingPromptService;\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Contracts\\Repositories\\PlaybookCategoryRepository;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\ModelNotFoundException;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Partner;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Repositories\\GroupRepository;\nuse Jiminny\\Repositories\\SearchRepository;\nuse Jiminny\\Repositories\\StageRepository;\nuse Throwable;\n\nclass AutomatedReportsService\n{\n public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';\n public const string TYPE_ASK_JIMINNY = 'ask_jiminny';\n\n /**\n * Standard report types (used by kiosk for existing automated reports).\n */\n // @TODO this will add filter, however if we need to control feature by FF we need conditional logic\n public const array TYPES = [\n ['id' => 'exec_summary', 'name' => 'Exec Summary'],\n ['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],\n ['id' => 'product_feedback', 'name' => 'Product Feedback'],\n ['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],\n// ['id' => 'questions', 'name' => 'Questions'],\n// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],\n ];\n\n public const array ALL_TYPES = [\n ...self::TYPES,\n ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],\n ];\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\n /**\n * Frequencies for standard (non-Ask Jiminny) reports.\n */\n public const array FREQUENCIES = [\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n\n /**\n * Frequencies for Ask Jiminny reports.\n */\n public const array ASK_JIMINNY_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ];\n\n public const string MEDIA_TYPE_PDF = 'pdf';\n public const string MEDIA_TYPE_PODCAST = 'podcast';\n public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];\n public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];\n public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];\n public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];\n\n public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];\n public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];\n public const int SENT_REPORT_AT_HOURS = 5;\n public const string PDF_KEY = 'pdf';\n public const string AUDIO_KEY = 'audio';\n\n private const array ALL_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n private const string S3_DIR = 'reports';\n private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];\n private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];\n\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly GroupRepository $groupRepository,\n private readonly UserRepository $userRepository,\n private readonly StageRepository $stageRepository,\n private readonly DealStagesService $dealStagesService,\n private readonly RecipientsService $recipientsService,\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly Webhook $webhookService,\n private readonly BusDispatcher $dispatcher,\n private readonly ActivityTypeService $activityTypeService,\n private readonly PlaybookCategoryRepository $playbookCategoryRepository,\n private readonly AskAnythingPromptService $askAnythingPromptService,\n private readonly SearchRepository $activitySearchRepository,\n private readonly AskAnythingRepository $askAnythingRepository,\n ) {\n }\n\n public static function getTypes(): array\n {\n $types = self::TYPES;\n\n return array_map(static function ($type) {\n return $type['id'];\n }, $types);\n }\n\n public static function getCallTypes(): array\n {\n return array_map(static function ($callType) {\n return $callType['id'];\n }, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);\n }\n\n public static function getFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::FREQUENCIES);\n }\n\n // front-facing structure\n public function getReportEnabledFieldData(bool $value = false): array\n {\n return [\n 'id' => 'report_enabled',\n 'label' => '',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'value' => $value,\n ];\n }\n\n // Organizations = Teams\n public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array\n {\n $options = $this->getTeams(partner: $partner);\n\n if ($shortVersion) {\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value,\n 'dependencies' => [\n 'teams',\n 'deal_stage_at_call',\n 'current_deal_stage',\n 'recipients',\n ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,\n ],\n 'dependsOn' => [],\n ];\n }\n\n // Teams = Groups\n public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array\n {\n if ($shortVersion) {\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value, // value should be an array of objects {id, name}\n 'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],\n 'dependsOn' => [],\n ];\n }\n\n public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array\n {\n $types = [];\n if ($team instanceof Team) {\n if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n $types = self::TYPES;\n }\n if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {\n $types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];\n }\n } else {\n $types = self::TYPES;\n }\n\n if ($shortVersion) {\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'options' => $types,\n ];\n }\n\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $types,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getFrequencyFieldData(?string $value = null): array\n {\n return [\n 'id' => 'frequency',\n 'label' => 'Frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::FREQUENCIES,\n 'value' => $value,\n 'dependencies' => ['period'],\n 'dependsOn' => [],\n ];\n }\n\n public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array\n {\n return [\n 'id' => 'period',\n 'label' => 'Select one-off period',\n 'inputType' => InputTypeEnum::DATE_RANGE,\n 'required' => true,\n 'placeholder' => 'Select',\n 'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],\n 'queryParams' => [\n 'startDate' => 'start_date_period',\n 'endDate' => 'end_date_period',\n ],\n 'dependencies' => [],\n 'dependsOn' => ['frequency'],\n ];\n }\n\n public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array\n {\n return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);\n }\n\n public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);\n }\n\n public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);\n }\n\n public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'deal_value',\n 'label' => 'Deal Value',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_deal_value',\n 'max' => 'max_deal_value',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array\n {\n $value = [];\n $conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;\n $dialerOn && $value[] = self::CALL_TYPE_DIALER;\n\n return [\n 'id' => 'call_type',\n 'label' => 'Call Type',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => [\n self::CALL_TYPE_CONFERENCE,\n self::CALL_TYPE_DIALER,\n ],\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getMediaTypeFieldData(?AutomatedReport $report = null): array\n {\n $value = [];\n\n if ($report) {\n $value = $this->transformMediaTypes($report);\n }\n\n return [\n 'id' => 'media_types',\n 'label' => 'Export as',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => self::MEDIA_TYPE_OBJECTS,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'call_duration',\n 'label' => 'Call Duration',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_call_duration',\n 'max' => 'max_call_duration',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getRecipientsFieldData(?Team $team = null, array $value = []): array\n {\n return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);\n }\n\n public function getJiminnyRecipientsFieldData(array $value = []): array\n {\n return $this->recipientsService->getJiminnyRecipientsFieldData($value);\n }\n\n public function getAdditionalPromptInputFieldData(?string $value = null): array\n {\n return [\n 'id' => 'additional_prompt_input',\n 'label' => 'Special requirements',\n 'inputType' => InputTypeEnum::TEXTAREA,\n 'required' => false,\n 'placeholder' => 'What should be the focus of the report?',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCustomReportNameFieldData(?string $value = null): array\n {\n return [\n 'id' => 'custom_name',\n 'label' => 'Custom report name',\n 'inputType' => InputTypeEnum::TEXT,\n 'required' => false,\n 'placeholder' => 'Enter custom name',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n // data providers\n public function getTeams(?Partner $partner = null): array\n {\n $teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);\n\n $teamData = [];\n foreach ($teams as $team) {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n continue;\n }\n\n $teamData[] = $this->transformTeam($team);\n }\n\n return $teamData;\n }\n\n public function getTeamGroups(string $teamUuid): array\n {\n $data = [];\n $team = $this->getTeam($teamUuid);\n\n if ($team !== null) {\n $groups = $team->groups()->get();\n\n foreach ($groups as $group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array\n {\n $data = [];\n $teams = $this->getTeams(partner: $partner);\n\n foreach ($teams as $team) {\n if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {\n continue;\n }\n\n $data[] = [\n 'label' => $team['name'],\n 'groups' => $this->getTeamGroups($team['id']),\n ];\n }\n\n return $data;\n }\n\n public function getTeam(string $teamUuid): ?Team\n {\n return $this->teamRepository->idOrUuid($teamUuid);\n }\n\n public function getTeamById(int $teamId): ?Team\n {\n return $this->teamRepository->find($teamId);\n }\n\n public function getGroupsUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportGroups = $report->getGroups();\n foreach ($reportGroups as $groupId) {\n if ($group = $this->groupRepository->find($groupId)) {\n $uuids[] = $group->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getPlaybookCategoriesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $playbookCategories = $report->getPlaybookCategories();\n foreach ($playbookCategories as $id) {\n if ($category = $this->playbookCategoryRepository->find($id)) {\n $uuids[] = $category->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getDealAtCallStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getDealAtCallStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getCurrentDealStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getCurrentDealStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getRecipients());\n }\n\n public function getJiminnyUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function extractUserUuids(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => $user->getUuid())\n ->values()\n ->all();\n }\n\n // get mail data\n public function getRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getRecipients());\n }\n\n /**\n * @return array<UserContract>\n */\n public function getRecipientUserObjects(AutomatedReport $report): array\n {\n $userIds = $report->getRecipients()['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->values()\n ->all();\n }\n\n private function getJiminnyRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function buildRecipientUsers(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => [\n 'email' => $user->getEmailAddress(),\n 'name' => $user->getName(),\n 'timezone' => $user->getTimezone()->getName(),\n ])\n ->values()\n ->all();\n }\n\n public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array\n {\n if ($report->isAskJiminnyReport()) {\n $recipients = $this->resolveAskJiminnyRecipients($report);\n } else {\n $recipients = $this->getRecipientUsers($report);\n if ($includeJiminny) {\n $recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));\n }\n }\n\n $emails = [];\n\n return array_values(array_filter(\n $recipients,\n static function ($recipient) use (&$emails) {\n if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {\n return false;\n }\n $emails[] = $recipient['email'];\n\n return true;\n }\n ));\n }\n\n private function resolveAskJiminnyRecipients(AutomatedReport $report): array\n {\n $recipients = [];\n\n $creator = $report->getCreator();\n if ($creator !== null) {\n $recipients[] = [\n 'email' => $creator->getEmailAddress(),\n 'name' => $creator->getName(),\n 'timezone' => $creator->getTimezone()->getName(),\n ];\n }\n\n return array_merge(\n $recipients,\n $this->buildRecipientUsers($report->getRecipients()),\n $this->getGroupRecipientUsers($report),\n );\n }\n\n private function getGroupRecipientUsers(AutomatedReport $report): array\n {\n $users = [];\n foreach ($report->getGroups() as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group === null) {\n continue;\n }\n foreach ($group->getMembers() as $member) {\n $users[] = [\n 'email' => $member->getEmailAddress(),\n 'name' => $member->getName(),\n 'timezone' => $member->getTimezone()->getName(),\n ];\n }\n }\n\n return $users;\n }\n\n public function getReportTypeName(AutomatedReportResult $report): string\n {\n $type = $report->getReport()->getType();\n\n $getType = $this->transformReportType($type);\n\n return $getType['name'];\n }\n\n public function getReportPeriodName(AutomatedReportResult $report): string\n {\n $from = $report->getFromDate();\n $to = $report->getToDate();\n $frequency = $report->getReport()->getFrequency();\n\n if ($from === null || $to === null) {\n if (! $report->getReport()->isAskJiminnyReport()) {\n $invalidPeriod = $from === null ? 'from' : 'to';\n\n throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);\n }\n\n $timezone = $report->getReport()->getCreator()?->getTimezone();\n $period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);\n $from = $period['fromDate'];\n $to = $period['toDate'];\n }\n\n return $this->formatReportPeriodName($frequency, $from, $to);\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 getReportTeamsName(AutomatedReportResult $report): string\n {\n $groups = $report->getGroups();\n\n if (empty($groups)) {\n return 'All';\n }\n\n // Get group names from repository\n $groupNames = [];\n foreach ($groups as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group) {\n $groupNames[] = $group->getName();\n }\n }\n\n if (count($groupNames) === 1) {\n // Single team format\n $teamsName = $groupNames[0];\n } else {\n // Multiple teams format\n $teamsName = implode(', ', $groupNames);\n }\n\n return $teamsName;\n }\n\n public function getReportFileName(AutomatedReportResult $report): string\n {\n $customName = $report->getReport()->getCustomName();\n $periodName = $this->getReportPeriodName($report);\n $filenameSuffix = $this->getFilenameSuffix($report);\n\n if ($customName) {\n if ($filenameSuffix) {\n $customName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$customName} - {$periodName}\");\n }\n\n $baseName = $this->getReportTypeName($report);\n\n if ($filenameSuffix) {\n $baseName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}\");\n }\n\n public function getReportFileNameWithExtension(AutomatedReportResult $result): string\n {\n $extension = $this->getMediaTypeMetadata($result)['extension'];\n\n return $this->getReportFileName($result) . '.' . $extension;\n }\n\n public function sanitizeFileName(string $fileName): string\n {\n return str_replace(['/', '\\\\'], '-', $fileName);\n }\n\n public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool\n {\n $recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);\n if (in_array($user->getId(), $recipientIds, true)) {\n return true;\n }\n\n if ($report->isAskJiminnyReport()) {\n $groupId = $user->getGroupId();\n if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {\n return true;\n }\n }\n\n return false;\n }\n\n public function transformReportResults(Collection $automatedReportResults): array\n {\n $data = [];\n foreach ($automatedReportResults as $automatedReportResult) {\n /** @var AutomatedReportResult $automatedReportResult */\n\n $report = $automatedReportResult->getReport();\n\n $createdBy = $report->getCreator();\n $creator = [\n 'id' => $createdBy?->getUuid(),\n 'name' => $createdBy?->getName(),\n 'email' => $createdBy?->getEmailAddress(),\n 'photoUrl' => $createdBy?->getPhotoUrl(),\n ];\n\n $data[] = [\n 'id' => $automatedReportResult->getUuid(),\n 'name' => $automatedReportResult->getName(),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n 'recipients' => $this->buildRecipients($report),\n 'report_type' => $this->transformReportType($report->getType()),\n 'media_type' => $automatedReportResult->getMediaType(),\n 'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),\n 'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),\n 'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),\n 'creator' => $creator,\n ];\n }\n\n return $data;\n }\n\n private function buildRecipients(AutomatedReport $report): array\n {\n $creatorUuid = $report->getCreator()?->getUuid();\n\n $recipients = array_values(array_filter(\n $this->transformRecipients($report->getRecipients()),\n static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,\n ));\n\n if (! $report->isAskJiminnyReport()) {\n return $recipients;\n }\n\n return [\n ...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),\n ...$recipients,\n ];\n }\n\n public function hasCallTypeConference(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);\n }\n\n public function hasCallTypeDialer(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);\n }\n\n // transformers\n private function transformTeam(Team $team): array\n {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n return [];\n }\n\n return [\n 'id' => $team->getUuid(),\n 'name' => $team->getName(),\n ];\n }\n\n private function transformReportFullView(AutomatedReport $report): array\n {\n $base = $this->transformReportBase($report);\n\n return $report->getType() === self::TYPE_ASK_JIMINNY\n ? $base + $this->transformAskJiminnyFields($report)\n : $base + $this->transformStandardReportFields($report);\n }\n\n private function transformReportBase(AutomatedReport $report): array\n {\n return [\n 'id' => $report->getUuid(),\n 'organization' => $this->transformOrganization(team: $report->getTeam()),\n 'report_type' => $this->transformReportType($report->getType()),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n ];\n }\n\n private function transformStandardReportFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n\n return [\n 'report_enabled' => $report->getStatus(),\n 'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),\n 'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),\n 'deal_value_min' => $report->getDealValueMin(),\n 'deal_value_max' => $report->getDealValueMax(),\n 'call_types' => $this->transformCallType($report->getCallTypes()),\n 'media_types' => $this->transformMediaTypes($report),\n 'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),\n 'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),\n 'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),\n 'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),\n 'recipients' => $this->transformRecipients($report->getRecipients()),\n 'created_by' => $this->transformCreator($report->getCreator()),\n 'additional_prompt_input' => $report->getAdditionalPromptInput(),\n 'custom_name' => $report->getCustomName(),\n 'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),\n 'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),\n 'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),\n ];\n }\n\n private function transformAskJiminnyFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n $creatorId = $report->getAttribute('created_by');\n $explicitUserIds = array_values(array_filter(\n $report->getRecipients()['users'] ?? [],\n static fn ($id) => $id !== $creatorId\n ));\n\n return [\n 'report_name' => $report->getCustomName(),\n 'enabled' => $report->getStatus(),\n 'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),\n 'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),\n 'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),\n 'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),\n ];\n }\n\n private function transformOrganization(?Team $team): array\n {\n return [\n 'id' => $team?->getUuid(),\n 'name' => $team?->getName(),\n ];\n }\n\n private function transformReportType(string $type): array\n {\n foreach (self::ALL_TYPES as $typeItem) {\n if ($typeItem['id'] === $type) {\n return $typeItem;\n }\n }\n\n return [];\n }\n\n private function transformCallType(array $types): array\n {\n $result = [];\n $callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];\n\n foreach ($types as $type) {\n foreach ($callTypes as $callTypeItem) {\n if ($callTypeItem['id'] === $type) {\n $result[] = $callTypeItem;\n\n break;\n }\n }\n }\n\n return $result;\n }\n\n private function transformMediaTypes(AutomatedReport $report): array\n {\n $values = [];\n\n foreach ($report->getMediaTypes() as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n continue;\n }\n\n $values[] = match ($mediaType) {\n self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,\n self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,\n };\n }\n\n return $values;\n }\n\n private function transformFrequency(string $frequency): array\n {\n foreach (self::ALL_FREQUENCIES as $frequencyItem) {\n if ($frequencyItem['id'] === $frequency) {\n return $frequencyItem;\n }\n }\n\n return [];\n }\n\n public function transformDurationToMinutes(?int $duration): ?int\n {\n if (! $duration) {\n return null;\n }\n\n return (int) ($duration / 60);\n }\n\n private function transformGroups(?Team $team, array $groupsIds): array\n {\n if (empty($groupsIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($groupsIds as $groupId) {\n $group = $team->groups()->where('id', $groupId)->first();\n\n if ($group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n 'photoUrl' => $group->getPhotoUrl(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformStages(?Team $team, array $stagesIds): array\n {\n if (empty($stagesIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($stagesIds as $stageId) {\n $stage = $team->stages()->where('id', $stageId)->first();\n\n if ($stage) {\n $data[] = [\n 'id' => $stage->getUuid(),\n 'name' => $stage->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformRecipients(array $recipients): array\n {\n $users = [];\n foreach ($recipients['users'] ?? [] as $userId) {\n $users[] = $this->transformUser($userId);\n }\n\n return $users;\n }\n\n private function transformCreator(?User $user): ?array\n {\n if ($user === null) {\n return null;\n }\n\n return $this->transformUser($user->getId());\n }\n\n private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array\n {\n if ($prompt === null) {\n return null;\n }\n\n return [\n 'id' => $prompt->getUuid(),\n 'name' => $prompt->getTitle(),\n ];\n }\n\n private function transformSafeSearch(?Search $search): ?array\n {\n if ($search === null) {\n return null;\n }\n\n return [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ];\n }\n\n private function transformUser(int $userId): array\n {\n /* @var ?User $user */\n $user = $this->userRepository->find($userId);\n\n return [\n 'id' => $user?->getUuid(),\n 'name' => $user?->getName(),\n 'email' => $user?->getEmailAddress(),\n 'photoUrl' => $user?->getPhotoUrl(),\n ];\n }\n\n public function create(array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $validatedData['created_by'] = auth()->id();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function update(string $uuid, array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $report = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $report) {\n throw new InvalidArgumentException('Report not found');\n }\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Create an Ask Jiminny report.\n */\n public function createAskJiminnyReport(array $data, User $creator): array\n {\n $validatedData = $this->validateAskJiminnyReportData($data, $creator);\n $validatedData['created_by'] = $creator->getId();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Update an Ask Jiminny report.\n */\n public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array\n {\n if (! $report->isAskJiminnyReport()) {\n throw new InvalidArgumentException('Report is not an Ask Jiminny report');\n }\n\n $validatedData = $this->validateAskJiminnyReportData($data, $user);\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array\n {\n if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {\n throw new InvalidArgumentException(\n 'This report is missing a saved search or prompt. ' .\n 'Edit the report to complete the setup before enabling it.'\n );\n }\n\n $this->automatedReportsRepository->update($report, ['status' => $status]);\n\n return $this->transformReportFullView($report->fresh());\n }\n\n /**\n * Validate and transform data for Ask Jiminny reports.\n */\n private function validateAskJiminnyReportData(array $data, User $user): array\n {\n // Validate name\n $name = trim($data['report_name'] ?? '');\n if (empty($name)) {\n throw new InvalidArgumentException('Report name is required');\n }\n if (mb_strlen($name) > 50) {\n throw new InvalidArgumentException('Report name must be 50 characters or less');\n }\n\n // Validate frequency (only daily, weekly, monthly for Ask Jiminny)\n $frequency = $data['frequency'] ?? null;\n $askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];\n if (! in_array($frequency, $askJiminnyFrequencies, true)) {\n throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');\n }\n\n // Validate expiration date\n $expiresAt = $data['expires_on'] ?? null;\n if (empty($expiresAt)) {\n throw new InvalidArgumentException('Expiration date is required');\n }\n\n try {\n $expiresAtDate = Carbon::parse($expiresAt);\n } catch (InvalidFormatException $e) {\n throw new InvalidArgumentException('Expiration date format is invalid');\n }\n $maxExpiration = Carbon::now()->addYear()->endOfDay();\n if ($expiresAtDate->gt($maxExpiration)) {\n throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');\n }\n if ($expiresAtDate->isPast()) {\n throw new InvalidArgumentException('Expiration date cannot be in the past');\n }\n\n // Validate saved search\n $activitySearchId = $data['saved_search'] ?? null;\n if (empty($activitySearchId)) {\n throw new InvalidArgumentException('Saved search is required');\n }\n $savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);\n if (! $savedSearch) {\n throw new InvalidArgumentException('Saved search not found or does not belong to you');\n }\n\n // Validate saved prompt\n $askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;\n if (empty($askAnythingPromptId)) {\n throw new InvalidArgumentException('Ask Jiminny prompt is required');\n }\n $prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);\n if (! $prompt) {\n throw new InvalidArgumentException('Ask Jiminny prompt not found');\n }\n\n // Validate status\n $status = $data['enabled'] ?? false;\n\n $recipientUserIds = [$user->getId()];\n\n if (! empty($data['share_users'])) {\n $sharedUserIds = $this->validateAndGetUserIdsByTeam(\n $user->team,\n (array) $data['share_users']\n );\n $recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);\n }\n\n $sharedGroupIds = [];\n if (! empty($data['share_teams'])) {\n $sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);\n }\n\n $recipientUserIds = array_values(array_unique($recipientUserIds));\n\n return [\n 'team_id' => $user->getTeamId(),\n 'type' => self::TYPE_ASK_JIMINNY,\n 'status' => (bool) $status,\n 'frequency' => $frequency,\n 'custom_name' => $name,\n 'activity_search_id' => $savedSearch->getId(),\n 'ask_anything_prompt_id' => $prompt->getId(),\n 'expires_at' => $expiresAtDate->toDateString(),\n 'media_types' => [self::MEDIA_TYPE_PDF],\n 'call_types' => [],\n 'recipients' => ['users' => $recipientUserIds],\n 'groups' => $sharedGroupIds,\n ];\n }\n\n public static function getAskJiminnyFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::ASK_JIMINNY_FREQUENCIES);\n }\n\n public function getAskJiminnyReportFilters(User $user): array\n {\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n return [\n [\n 'id' => 'prompt',\n 'label' => 'Prompt',\n 'options' => $prompts,\n ],\n [\n 'id' => 'saved_search',\n 'label' => 'Saved Search',\n 'options' => $savedSearches,\n ],\n ];\n }\n\n public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array\n {\n $team = $user->getTeam();\n $userTimezone = $user->getTimezone();\n\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n $teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ])->values()->all();\n\n $shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];\n\n $sharedTeamsValue = [];\n $sharedUsersValue = [];\n if ($report) {\n $sharedTeamsValue = $this->transformGroups($team, $report->getGroups());\n\n $recipientUserIds = $report->getRecipients()['users'] ?? [];\n $creatorId = $report->getAttribute('created_by');\n $sharedUserIds = array_values(array_filter(\n $recipientUserIds,\n static fn ($id) => $id !== $creatorId\n ));\n $sharedUsersValue = collect($sharedUserIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (User $u) => [\n 'id' => $u->getUuid(),\n 'name' => $u->getName(),\n ])\n ->values()\n ->all();\n }\n\n return [\n 'fields' => [\n [\n 'id' => 'enabled',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'label' => '',\n 'value' => $report?->getStatus() ?? false,\n ],\n [\n 'id' => 'report_name',\n 'inputType' => InputTypeEnum::TEXT,\n 'label' => 'Name',\n 'placeholder' => 'Enter name',\n 'required' => true,\n 'validation' => ['maxLength' => 50],\n 'value' => $report?->getCustomName() ?? '',\n ],\n [\n 'id' => 'frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Frequency',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::ASK_JIMINNY_FREQUENCIES,\n 'value' => $report ? $this->transformFrequency($report->getFrequency()) : null,\n ],\n [\n 'id' => 'expires_on',\n 'inputType' => InputTypeEnum::DATE,\n 'label' => 'Expires on',\n 'required' => true,\n 'placeholder' => 'Select',\n 'validation' => [\n 'minDate' => now($userTimezone)->toDateString(),\n 'maxDate' => now($userTimezone)->addYear()->toDateString(),\n ],\n 'value' => $report?->getExpiresAt()?->toDateString(),\n ],\n [\n 'id' => 'share_teams',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team',\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $teamGroups,\n 'value' => $sharedTeamsValue,\n ],\n [\n 'id' => 'share_users',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team member',\n 'required' => false,\n 'placeholder' => 'Select',\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'options' => $shareUsers,\n 'value' => $sharedUsersValue,\n ],\n [\n 'id' => 'saved_search',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Saved search',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $savedSearches,\n 'value' => $report && $report->getSavedSearch() ? [\n 'id' => $report->getSavedSearch()->getUuid(),\n 'name' => $report->getSavedSearch()->getName(),\n ] : null,\n ],\n [\n 'id' => 'ask_jiminny_prompt',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Ask Jiminny prompt',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $prompts,\n 'value' => $report && $report->getAskAnythingPrompt() ? [\n 'id' => $report->getAskAnythingPrompt()->getUuid(),\n 'name' => $report->getAskAnythingPrompt()->getTitle(),\n ] : null,\n ],\n ],\n ];\n }\n\n private function updateResultNames(AutomatedReport $automatedReport): void\n {\n $results = $this->automatedReportsRepository->getResultsByReport($automatedReport);\n\n foreach ($results as $result) {\n $result->update(['name' => $this->getReportFileName($result)]);\n }\n }\n\n public function updateStatus(string $uuid, array $data): array\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $automatedReport->update([\n 'status' => $status,\n ]);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n private function generateOneOffReport(AutomatedReport $automatedReport): void\n {\n // the scheduler handles all the other frequency types\n if ($automatedReport->getStatus() === false || $automatedReport->getFrequency() !== self::FREQUENCY_ONE_OFF) {\n return;\n }\n\n $this->dispatcher->dispatch(new RequestGenerateReportJob($automatedReport->getUuid()));\n }\n\n public function getReport(string $uuid, ?Partner $partner = null): AutomatedReport\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n if ($partner !== null && ! $partner->isDefaultPartner() && $automatedReport->team->partner_id !== $partner->getId()) {\n throw new ModelNotFoundException('Report not found');\n }\n\n return $automatedReport;\n }\n\n public function get(string $uuid, ?Partner $partner = null): array\n {\n $automatedReport = $this->getReport($uuid, $partner);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function list(string $sortColumn = 'created_at', string $sortDirection = 'desc', ?Partner $partner = null): array\n {\n $results = [];\n $collection = $this->automatedReportsRepository->getAllStandardReports($sortColumn, $sortDirection, $partner);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function listAskJiminnyReports(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): array {\n $results = [];\n $collection = $this->automatedReportsRepository->getAskJiminnyReportsByUser($user, $sortColumn, $sortDirection);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function delete(string $uuid): void\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $automatedReport->delete();\n }\n\n public function createReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n return $this->automatedReportsRepository->createResult(\n array_merge(\n [\n 'report_id' => $automatedReport->getId(),\n 'status' => AutomatedReportResult::STATUS_DEFAULT,\n ],\n $data\n )\n );\n }\n\n public function getOrCreateReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n $existing = $this->automatedReportsRepository->findLatestSameDayDefaultOrFailedResult($automatedReport);\n\n if ($existing !== null) {\n $existing->update(['status' => AutomatedReportResult::STATUS_DEFAULT]);\n\n return $existing;\n }\n\n return $this->createReportResult($automatedReport, $data);\n }\n\n public function getReportResult(string $resultUuid): AutomatedReportResult\n {\n $report = $this->automatedReportsRepository->findResultByUuid($resultUuid);\n\n if (! $report) {\n throw new ModelNotFoundException('Report Result not found');\n }\n\n return $report;\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return $this->automatedReportsRepository->findChildResult($result, $type);\n }\n\n // prophet API calls\n /**\n * @throws ApplicationException\n */\n public function getGenerateReportPayload(AutomatedReport $automatedReport, string $reportResultUuid): array\n {\n $period = $this->calculateFromAndToDate($automatedReport);\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n return [\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResultUuid,\n 'report_type' => $automatedReport->getType(),\n 'media_types' => $automatedReport->getMediaTypes(),\n 'from_date' => $fromDate->startOfDay()->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->endOfDay()->format(DateTimeInterface::RFC3339),\n 'group_ids' => $automatedReport->getGroups(),\n 'call_deal_stage' => $automatedReport->getDealAtCallStages(),\n 'current_deal_stage' => $automatedReport->getCurrentDealStages(),\n 'deal_min_value' => $automatedReport->getDealValueMin(),\n 'deal_max_value' => $automatedReport->getDealValueMax(),\n 'call_types' => $automatedReport->getCallTypes(),\n 'call_duration_min_seconds' => $automatedReport->getCallDurationMin(),\n 'call_duration_max_seconds' => $automatedReport->getCallDurationMax(),\n 'special_requirements' => $automatedReport->getAdditionalPromptInput(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->formatReportPeriodName(\n $automatedReport->getFrequency(),\n $fromDate,\n $toDate,\n ),\n 'playbook_categories' => $automatedReport->getPlaybookCategories(),\n 'custom_name' => $automatedReport->getCustomName(),\n ];\n }\n\n // $inputPayload - FE payload structure\n public function getActivitiesCountPayload(array $inputPayload): array\n {\n // Use validateAndTransformData to validate and normalize input\n $validatedData = $this->validateAndTransformData($inputPayload);\n $period = $this->calculateFromAndToDatePeriod(\n $validatedData['frequency'],\n Carbon::parse($validatedData['from']),\n Carbon::parse($validatedData['to']),\n );\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n // Create payload similar to getGenerateReportPayload\n return [\n 'team_id' => $validatedData['team_id'],\n 'group_ids' => $validatedData['groups'] ?? [],\n 'report_type' => $validatedData['type'],\n 'from_date' => $fromDate->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->format(DateTimeInterface::RFC3339),\n 'call_deal_stage' => $validatedData['deal_at_call_stages'] ?? [],\n 'current_deal_stage' => $validatedData['current_deal_stages'] ?? [],\n 'deal_min_value' => $validatedData['deal_value_min'] ?? null,\n 'deal_max_value' => $validatedData['deal_value_max'] ?? null,\n 'call_types' => $validatedData['call_types'],\n 'call_duration_min_seconds' => $validatedData['call_duration_min'] ?? null,\n 'call_duration_max_seconds' => $validatedData['call_duration_max'] ?? null,\n 'special_requirements' => $validatedData['additional_prompt_input'] ?? null,\n 'playbook_categories' => $validatedData['playbook_categories'] ?? [],\n 'request_id' => null,\n 'callback_url' => null,\n ];\n }\n\n public function shouldSendReport(array $users, ?CarbonInterface $generatedAt = null): bool\n {\n if (empty($users)) {\n return false;\n }\n\n $earliestTz = collect($users)\n ->mapWithKeys(function (array $user) {\n $tz = new DateTimeZone($user['timezone']);\n $nowUtc = new DateTime('now', new DateTimeZone('UTC'));\n $offset = $tz->getOffset($nowUtc);\n\n return [$user['timezone'] => $offset];\n })\n ->sortDesc()\n ->keys()\n ->first();\n\n $now = Carbon::now($earliestTz);\n $isScheduledTime = (int) $now->format('H') === self::SENT_REPORT_AT_HOURS;\n\n if ($isScheduledTime) {\n return true;\n }\n\n return $this->hasPassedScheduledTime($generatedAt, $earliestTz);\n }\n\n public function hasPassedScheduledTime(?CarbonInterface $generatedAt, string $timezone): bool\n {\n if ($generatedAt === null) {\n return false;\n }\n\n $now = Carbon::now($timezone);\n $scheduledTime = $now->copy()->setTime(self::SENT_REPORT_AT_HOURS, 0, 0);\n\n if ($now->hour < self::SENT_REPORT_AT_HOURS) {\n $scheduledTime = $scheduledTime->subDay();\n }\n\n $scheduledTimeUtc = $scheduledTime->copy()->utc();\n $generatedAtUtc = $generatedAt->copy()->utc();\n $nowUtc = $now->copy()->utc();\n\n return $generatedAtUtc->lt($scheduledTimeUtc) && $nowUtc->gt($scheduledTimeUtc);\n }\n\n public function calculateFromAndToDatePeriod(\n string $frequency,\n ?Carbon $fromDate = null,\n ?Carbon $toDate = null,\n DateTimeZone|string|null $timezone = 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($timezone);\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()->subWeek()->startOfWeek(CarbonInterface::MONDAY),\n 'toDate' => $now->copy()->subWeek()->endOfWeek(CarbonInterface::SUNDAY),\n ],\n self::FREQUENCY_MONTHLY => [\n 'fromDate' => $now->copy()->subMonthNoOverflow()->startOfMonth(),\n 'toDate' => $now->copy()->subMonthNoOverflow()->endOfMonth(),\n ],\n self::FREQUENCY_QUARTERLY => [\n 'fromDate' => $now->copy()->subQuarterNoOverflow()->startOfQuarter(),\n 'toDate' => $now->copy()->subQuarterNoOverflow()->endOfQuarter(),\n ],\n default => throw new InvalidArgumentException(\"Unsupported frequency: {$frequency}\"),\n };\n }\n\n private function calculateFromAndToDate(AutomatedReport $automatedReport): array\n {\n return $this->calculateFromAndToDatePeriod(\n $automatedReport->getFrequency(),\n $automatedReport->getFrom(),\n $automatedReport->getTo()\n );\n }\n\n public function getAskJiminnyGenerateReportPayload(\n AutomatedReport $automatedReport,\n AutomatedReportResult $reportResult,\n array $activityIds,\n ): array {\n return [\n 'user_question' => $automatedReport->getAskAnythingPrompt()?->getContent(),\n 'call_ids' => array_map('strval', $activityIds),\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResult->getUuid(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->getReportPeriodName($reportResult),\n 'report_name' => $automatedReport->getCustomName(),\n ];\n }\n\n private function getCallbackUrl(): string\n {\n return $this->webhookService->route('jiminny.webhook.reports.ready');\n }\n\n /**\n * Validate and transform payload data for automated reports\n *\n * @param array $data\n *\n * @throws InvalidArgumentException\n *\n * @return array\n */\n private function validateAndTransformData(array $data): array\n {\n // Validate organization (team) and check feature\n $team = $this->validateOrganization($data['organization'] ?? null);\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $type = $this->validateReportType($data['report_type'] ?? null);\n $frequency = $this->validateFrequency($data['frequency'] ?? null);\n $additionalPromptInput = $this->validateAdditionalPromptInput(\n $data['additional_prompt_input'] ?? null\n );\n $customReportName = $this->validateCustomReportName($data['custom_name'] ?? null);\n\n // Prepare data for the database\n $reportData = [\n 'team_id' => $team->getId(),\n 'type' => $type,\n 'status' => $status,\n 'frequency' => $frequency,\n 'additional_prompt_input' => $additionalPromptInput,\n 'custom_name' => $customReportName,\n ];\n\n // Validate deal values\n $reportData = $this->validateDealValues($data, $reportData);\n\n // Validate date range\n $reportData = $this->validateDateRange($data, $reportData, $frequency);\n\n // Validate call durations\n $reportData = $this->validateCallDurations($data, $reportData);\n\n // Validate call types\n $reportData = $this->validateCallTypes($data, $reportData);\n\n // Validate media types\n $reportData = $this->validateMediaTypes($data, $reportData);\n\n // Validate groups\n if (isset($data['teams'])) {\n $reportData['groups'] = $this->validateAndGetGroupIds($team, $data['teams']);\n }\n\n // Validate deal stages\n $reportData = $this->validateDealStages($data, $reportData, $team, $type);\n\n // Validate playbook categories\n $reportData = $this->validatePlaybookCategories($data, $reportData, $team);\n\n // Validate recipients\n $reportData['recipients'] = [\n 'users' => $this->validateAndGetUserIdsByTeam($team, $data['recipients'] ?? []),\n ];\n\n if (isset($data['jiminny_recipients'])) {\n // Validate Jiminny recipients\n $reportData['jiminny_recipients'] = [\n 'users' => $this->validateAndGetJiminnyUserIds((array) $data['jiminny_recipients']),\n ];\n }\n\n return $reportData;\n }\n\n private function validateDealValues(array $data, array $reportData): array\n {\n if (isset($data['min_deal_value'])) {\n $reportData['deal_value_min'] = (int) $data['min_deal_value'];\n\n if ($reportData['deal_value_min'] > 4294967295 || $reportData['deal_value_min'] < 0) {\n throw new InvalidArgumentException('Min deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_deal_value'])) {\n $reportData['deal_value_max'] = (int) $data['max_deal_value'];\n\n if ($reportData['deal_value_max'] > 4294967295 || $reportData['deal_value_max'] < 0) {\n throw new InvalidArgumentException('Max deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['min_deal_value'], $data['max_deal_value'])\n && $data['min_deal_value'] > $data['max_deal_value']\n ) {\n throw new InvalidArgumentException('Min deal value cannot be greater than max deal value');\n }\n\n return $reportData;\n }\n\n private function validateDateRange(array $data, array $reportData, string $frequency): array\n {\n // Set date range only for one_off frequency\n if ($frequency === 'one_off') {\n if (isset($data['start_date_period'])) {\n $reportData['from'] = $this->parseDate($data['start_date_period']);\n }\n\n if (isset($data['end_date_period'])) {\n $reportData['to'] = $this->parseDate($data['end_date_period']);\n }\n\n if (empty($reportData['from']) || empty($reportData['to'])) {\n throw new InvalidArgumentException(\n 'Start date and end date are required for one_off frequency'\n );\n }\n } else {\n $reportData['from'] = null;\n $reportData['to'] = null;\n }\n\n return $reportData;\n }\n\n private function validateCallDurations(array $data, array $reportData): array\n {\n // Convert call durations from minutes to seconds\n if (isset($data['min_call_duration'])) {\n $reportData['call_duration_min'] = (int) $data['min_call_duration'] * 60;\n\n if ($reportData['call_duration_min'] > 4294967295 || $reportData['call_duration_min'] < 0) {\n throw new InvalidArgumentException('Min call duration should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_call_duration'])) {\n $reportData['call_duration_max'] = (int) $data['max_call_duration'] * 60;\n\n if ($reportData['call_duration_max'] > 4294967295 || $reportData['call_duration_max'] < 0) {\n throw new InvalidArgumentException('Max call duration should be between 0 and 4294967295');\n }\n }\n\n return $reportData;\n }\n\n private function validateCallTypes(array $data, array $reportData): array\n {\n // Set call types\n $reportData['call_types'] = $data['call_type'] ?? [];\n if (empty($reportData['call_types'])) {\n $reportData['call_types'] = self::getCallTypes();\n }\n\n foreach ($reportData['call_types'] as $callType) {\n if (! in_array($callType, self::getCallTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Call type %s is invalid', $callType));\n }\n }\n\n return $reportData;\n }\n\n private function validateMediaTypes(array $data, array $reportData): array\n {\n // Set media types from input data\n $reportData['media_types'] = $data['media_types'] ?? [];\n\n if (empty($reportData['media_types'])) {\n throw new InvalidArgumentException('Media types are required');\n }\n\n foreach ($reportData['media_types'] as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n throw new InvalidArgumentException(sprintf('Media type %s is invalid', $mediaType));\n }\n }\n\n return $reportData;\n }\n\n private function validateDealStages(array $data, array $reportData, Team $team, string $reportType): array\n {\n // Validate and set deal stages\n if (isset($data['deal_stage_at_call'])) {\n $reportData['deal_at_call_stages'] =\n $this->validateAndGetDealStageIds($team, $data['deal_stage_at_call'], 'Deal stage at call');\n }\n\n if (isset($data['current_deal_stage'])) {\n $reportData['current_deal_stages'] =\n $this->validateAndGetDealStageIds($team, $data['current_deal_stage'], 'Current deal stage');\n }\n\n // Ensure current_deal_stage is not provided for loss_analysis report type\n if ($reportType === self::TYPE_LOSS_ANALYSIS && ! empty($data['current_deal_stage'])) {\n throw new InvalidArgumentException('Current deal stage is not applicable for Loss Analysis reports');\n }\n\n return $reportData;\n }\n\n // transform uuid to id\n private function validatePlaybookCategories(array $data, array $reportData, Team $team): array\n {\n $key = 'playbook_categories';\n\n if (isset($data[$key])) {\n $payloadIds = $data[$key];\n $ids = [];\n\n foreach ($payloadIds as $uuid) {\n $uuid = (string) $uuid;\n\n try {\n $playbookCategory = $this->playbookCategoryRepository->findByUuid($uuid);\n } catch (Throwable $throwable) {\n Log::error(__METHOD__ . ' ' . $throwable->getMessage());\n\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory) {\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory->hasPlaybook()) {\n throw new InvalidArgumentException(sprintf('Playbook category %s has no playbook', $uuid));\n }\n\n if ($playbookCategory->getPlaybook()->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Playbook category %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $ids[] = $playbookCategory->getId();\n }\n\n $reportData[$key] = $ids;\n }\n\n return $reportData;\n }\n\n private function validateReportStatus($status): bool\n {\n if (! in_array($status, [true, false], true)) {\n throw new InvalidArgumentException('Report status is invalid');\n }\n\n return $status;\n }\n\n private function validateReportType($type): string\n {\n if (! in_array($type, self::getTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Report type is invalid: %s', $type));\n }\n\n return $type;\n }\n\n private function validateFrequency($frequency): string\n {\n if (! in_array($frequency, self::getFrequencies(), true)) {\n throw new InvalidArgumentException('Frequency is invalid');\n }\n\n return $frequency;\n }\n\n private function validateAdditionalPromptInput(?string $additionalPromptInput): ?string\n {\n if ($additionalPromptInput && strlen($additionalPromptInput) > 5000) {\n throw new InvalidArgumentException('Additional Prompt Input should be less than 5000 characters');\n }\n\n return $additionalPromptInput;\n }\n\n private function validateCustomReportName(?string $customReportName): ?string\n {\n if ($customReportName === null || $customReportName === '') {\n return null;\n }\n\n if (strlen($customReportName) > 70) {\n throw new InvalidArgumentException('Custom report name should be less than 70 characters');\n }\n\n return $customReportName;\n }\n\n private function validateOrganization(?string $organizationUuid): Team\n {\n if (! $organizationUuid) {\n throw new InvalidArgumentException('Organization is required');\n }\n\n $team = $this->teamRepository->idOrUuid($organizationUuid);\n\n if (! $team) {\n throw new InvalidArgumentException('Organization not found');\n }\n\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n throw new InvalidArgumentException('Organization does not have the Automated Reports feature');\n }\n\n return $team;\n }\n\n private function validateAndGetGroupIds(Team $team, array $teamUuids): array\n {\n $groupIds = [];\n\n foreach ($teamUuids as $uuid) {\n $group = $this->groupRepository->findByUuid($uuid);\n\n if ($group === null || $group->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Group %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $groupIds[] = $group->getId();\n\n }\n\n return $groupIds;\n }\n\n private function validateAndGetDealStageIds(Team $team, array $stageUuids, string $propertyLabel): array\n {\n $stageIds = [];\n\n foreach ($stageUuids as $uuid) {\n $stage = $this->stageRepository->findByUuid($uuid);\n\n if ($stage === null || $stage->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Stage %s not found for team %s for %s', $uuid, $team->getUuid(), $propertyLabel)\n );\n }\n\n $stageIds[] = $stage->getId();\n }\n\n return $stageIds;\n }\n\n private function validateAndGetUserIds(array $userUuids, callable $teamCheck): array\n {\n if (empty($userUuids)) {\n return [];\n }\n\n $userIds = [];\n\n foreach ($userUuids as $uuid) {\n $user = $this->userRepository->findByUuid($uuid);\n\n if (! $user || ! $user->isStatusActive()) {\n throw new InvalidArgumentException(\n sprintf('User %s not found or is not active', $uuid)\n );\n }\n\n if (! $teamCheck($user)) {\n throw new InvalidArgumentException(\n sprintf('User %s does not belong to the allowed team(s)', $uuid)\n );\n }\n\n $userIds[] = $user->getId();\n }\n\n return $userIds;\n }\n\n private function validateAndGetUserIdsByTeam(Team $team, array $userUuids): array\n {\n return $this->validateAndGetUserIds($userUuids, fn ($user) => $user->getTeamId() === $team->getId());\n }\n\n private function validateAndGetJiminnyUserIds(array $userUuids): array\n {\n $allowedTeamIds = config('kiosk.teamIds', []);\n\n return $this->validateAndGetUserIds($userUuids, fn ($user) => in_array($user->getTeamId(), $allowedTeamIds, true));\n }\n\n private function parseDate(string $dateString): string\n {\n return date('Y-m-d H:i:s', strtotime($dateString));\n }\n\n private function generateReportResultViewUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.view', ['uuid' => $result->getUuid()]);\n }\n\n private function generateReportResultDownloadUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.download', ['uuid' => $result->getUuid()]);\n }\n\n private function getReportMediaRouteResource(AutomatedReportResult $result): string\n {\n if ($result->getMediaType() === self::MEDIA_TYPE_PDF) {\n return self::PDF_KEY;\n } elseif ($result->getMediaType() === self::MEDIA_TYPE_PODCAST) {\n return self::AUDIO_KEY;\n }\n\n throw new \\InvalidArgumentException('Unknown media type.');\n }\n\n public function getMediaPath(AutomatedReportResult $result): ?string\n {\n $url = match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => $result->getPdfUrl(),\n self::MEDIA_TYPE_PODCAST => $result->getPodcastAudioUrl(),\n default => null,\n };\n\n if ($url === null) {\n return null;\n }\n\n $path = parse_url(trim($url, '\"\\''), PHP_URL_PATH);\n\n return $path ?: null;\n }\n\n public function getFilenameSuffix(AutomatedReportResult $result): ?string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => 'Podcast',\n default => null,\n };\n }\n\n public function getMailSubjectSuffix(AutomatedReportResult $result): string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => 'report',\n self::MEDIA_TYPE_PODCAST => 'podcast',\n default => '',\n };\n }\n\n public function getMediaTypeMetadata(AutomatedReportResult $result): array\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => ['extension' => 'mp3', 'mime' => 'audio/mpeg'],\n self::MEDIA_TYPE_PDF => ['extension' => 'pdf', 'mime' => 'application/pdf'],\n default => ['extension' => null, 'mime' => null],\n };\n }\n\n public function deleteS3Files(AutomatedReportResult $result): void\n {\n $teamUuid = $result->getReport()->getTeam()->getUuid();\n $reportUuid = $result->getUuid();\n\n // delete all files for a report uuid no mather of pdf, podcast, or both\n // in case of both - the podcast files are linked to the pdf (parent) uuid\n // pdf and podcast date times should be close\n $path = sprintf('%s/%s/%s', $teamUuid, self::S3_DIR, $reportUuid);\n\n foreach (self::FILE_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted S3 file', [\n 'path' => $file,\n ]);\n }\n }\n\n foreach (self::FILE_PODCAST_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '_podcast.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted Podcast S3 file', [\n 'path' => $file,\n ]);\n }\n }\n }\n\n /**\n *\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): Collection\n {\n return $this->automatedReportsRepository->getTeamIdsWithReportsResults($teamId);\n }\n\n /**\n * Core delete logic for report results using a query\n *\n * @param Builder $query\n * @param array $logContext\n *\n * @return int\n */\n private function deleteReportResultsByQuery(Builder $query, array $logContext = []): int\n {\n $deletedCount = 0;\n\n if ($query->exists()) {\n Log::info(\n 'Run delete report results',\n array_merge(\n $logContext,\n [\n 'service' => 'AutomatedReportsService',\n ]\n )\n );\n\n $query->chunkById(50, function ($results) use (&$deletedCount, $logContext) {\n foreach ($results as $result) {\n $this->deleteReportResult($result);\n $deletedCount++;\n\n Log::info(\n 'Deleted a report result',\n array_merge(\n $logContext,\n [\n 'result_id' => $result->getId(),\n 'report_id' => $result->getReportId(),\n ]\n )\n );\n }\n });\n }\n\n return $deletedCount;\n }\n\n /**\n * Delete report results for a team by retention period\n *\n * @param Team $team\n * @param CarbonImmutable $retentionDate\n *\n * @return int Number of deleted report results\n */\n public function deleteReportsResultsInRetentionPeriod(Team $team, CarbonImmutable $retentionDate): int\n {\n $reportIds = $this->automatedReportsRepository->getReportIdsByTeam($team);\n\n if ($reportIds->isEmpty()) {\n return 0;\n }\n\n $query = $this->automatedReportsRepository\n ->getReportResultsQueryForRetention($team, $retentionDate);\n\n return $this->deleteReportResultsByQuery($query, [\n 'team_id' => $team->getId(),\n 'retention_date' => $retentionDate->toDateTimeString(),\n ]);\n }\n\n /**\n * Delete ALL report results for a specific automated report\n *\n * @param string $uuid\n *\n * @return int\n */\n public function deleteReportResults(string $uuid): int\n {\n $report = $this->getReport($uuid);\n\n $query = $this->automatedReportsRepository->getResultsByReportQuery($report);\n\n return $this->deleteReportResultsByQuery($query, [\n 'report_uuid' => $uuid,\n 'report_id' => $report->getId(),\n ]);\n }\n\n public function deleteReportResult(AutomatedReportResult $result): void\n {\n $this->deleteS3Files($result);\n\n $result->delete();\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getTeamReports(Team $team): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getReportsByTeam($team);\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getReportResults(AutomatedReport $report): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getResultsByReport($report);\n }\n\n public function deleteAllReportResults(AutomatedReport $report): void\n {\n $results = $this->getReportResults($report);\n\n /** @var AutomatedReportResult $result */\n foreach ($results as $result) {\n Log::info('Deleting result', [\n 'report' => $report->getId(),\n 'result' => $result->getId(),\n ]);\n\n $this->deleteReportResult($result);\n }\n }\n\n public function deleteAllData(Team $team): void\n {\n Log::info('Deleting automated report and results for team', [\n 'team' => $team->getId(),\n ]);\n\n $reports = $this->getTeamReports($team);\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n Log::info('Deleting report', [\n 'team' => $team->getId(),\n 'report' => $report->getId(),\n ]);\n\n $this->deleteAllReportResults($report);\n\n $report->delete();\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.42785904,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.43650267,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.4474734,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.45611703,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.46476063,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.47573137,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.4867021,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"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.51329786,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.5242686,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.70611703,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"21","depth":4,"bounds":{"left":0.66921544,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.68085104,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"18","depth":4,"bounds":{"left":0.69015956,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.7017952,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.7117686,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.7287234,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"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 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 * 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 `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\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;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","depth":4,"on_screen":true,"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 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 * 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 `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\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;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Socket fail to connect to host:address=(host=localhost)(port=3306)(type=primary). Connection refused","depth":3,"bounds":{"left":0.42652926,"top":0.9577015,"width":0.29321808,"height":0.013567438},"on_screen":true,"value":"Socket fail to connect to host:address=(host=localhost)(port=3306)(type=primary). Connection refused","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1751808696220340334
|
1126710648141684156
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskAnythingPromptServiceTest
Run 'AskAnythingPromptServiceTest'
Debug 'AskAnythingPromptServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
100
3
34
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Kiosk\AutomatedReports;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Carbon\Exceptions\InvalidFormatException;
use DateTime;
use DateTimeInterface;
use DateTimeZone;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\ActivitySearch\FilterDefinition\InputTypeEnum;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Partner;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Throwable;
class AutomatedReportsService
{
public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';
public const string TYPE_ASK_JIMINNY = 'ask_jiminny';
/**
* Standard report types (used by kiosk for existing automated reports).
*/
// @TODO this will add filter, however if we need to control feature by FF we need conditional logic
public const array TYPES = [
['id' => 'exec_summary', 'name' => 'Exec Summary'],
['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],
['id' => 'product_feedback', 'name' => 'Product Feedback'],
['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],
// ['id' => 'questions', 'name' => 'Questions'],
// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],
];
public const array ALL_TYPES = [
...self::TYPES,
['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],
];
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';
/**
* Frequencies for standard (non-Ask Jiminny) reports.
*/
public const array FREQUENCIES = [
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
/**
* Frequencies for Ask Jiminny reports.
*/
public const array ASK_JIMINNY_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
];
public const string MEDIA_TYPE_PDF = 'pdf';
public const string MEDIA_TYPE_PODCAST = 'podcast';
public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];
public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];
public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];
public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];
public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];
public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];
public const int SENT_REPORT_AT_HOURS = 5;
public const string PDF_KEY = 'pdf';
public const string AUDIO_KEY = 'audio';
private const array ALL_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
private const string S3_DIR = 'reports';
private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];
private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly GroupRepository $groupRepository,
private readonly UserRepository $userRepository,
private readonly StageRepository $stageRepository,
private readonly DealStagesService $dealStagesService,
private readonly RecipientsService $recipientsService,
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly Webhook $webhookService,
private readonly BusDispatcher $dispatcher,
private readonly ActivityTypeService $activityTypeService,
private readonly PlaybookCategoryRepository $playbookCategoryRepository,
private readonly AskAnythingPromptService $askAnythingPromptService,
private readonly SearchRepository $activitySearchRepository,
private readonly AskAnythingRepository $askAnythingRepository,
) {
}
public static function getTypes(): array
{
$types = self::TYPES;
return array_map(static function ($type) {
return $type['id'];
}, $types);
}
public static function getCallTypes(): array
{
return array_map(static function ($callType) {
return $callType['id'];
}, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);
}
public static function getFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::FREQUENCIES);
}
// front-facing structure
public function getReportEnabledFieldData(bool $value = false): array
{
return [
'id' => 'report_enabled',
'label' => '',
'inputType' => InputTypeEnum::TOGGLE,
'value' => $value,
];
}
// Organizations = Teams
public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array
{
$options = $this->getTeams(partner: $partner);
if ($shortVersion) {
return [
'id' => 'organization',
'label' => 'Organization',
'options' => $options,
];
}
return [
'id' => 'organization',
'label' => 'Organization',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $options,
'value' => $value,
'dependencies' => [
'teams',
'deal_stage_at_call',
'current_deal_stage',
'recipients',
ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,
],
'dependsOn' => [],
];
}
// Teams = Groups
public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array
{
if ($shortVersion) {
return [
'id' => 'teams',
'label' => 'Team',
'options' => $options,
];
}
return [
'id' => 'teams',
'label' => 'Team',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => false,
'placeholder' => 'Select',
'options' => $options,
'value' => $value, // value should be an array of objects {id, name}
'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],
'dependsOn' => [],
];
}
public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array
{
$types = [];
if ($team instanceof Team) {
if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
$types = self::TYPES;
}
if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {
$types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];
}
} else {
$types = self::TYPES;
}
if ($shortVersion) {
return [
'id' => 'report_type',
'label' => 'Report Type',
'options' => $types,
];
}
return [
'id' => 'report_type',
'label' => 'Report Type',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $types,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getFrequencyFieldData(?string $value = null): array
{
return [
'id' => 'frequency',
'label' => 'Frequency',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => self::FREQUENCIES,
'value' => $value,
'dependencies' => ['period'],
'dependsOn' => [],
];
}
public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array
{
return [
'id' => 'period',
'label' => 'Select one-off period',
'inputType' => InputTypeEnum::DATE_RANGE,
'required' => true,
'placeholder' => 'Select',
'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],
'queryParams' => [
'startDate' => 'start_date_period',
'endDate' => 'end_date_period',
],
'dependencies' => [],
'dependsOn' => ['frequency'],
];
}
public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array
{
return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);
}
public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);
}
public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);
}
public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'deal_value',
'label' => 'Deal Value',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_deal_value',
'max' => 'max_deal_value',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array
{
$value = [];
$conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;
$dialerOn && $value[] = self::CALL_TYPE_DIALER;
return [
'id' => 'call_type',
'label' => 'Call Type',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => [
self::CALL_TYPE_CONFERENCE,
self::CALL_TYPE_DIALER,
],
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getMediaTypeFieldData(?AutomatedReport $report = null): array
{
$value = [];
if ($report) {
$value = $this->transformMediaTypes($report);
}
return [
'id' => 'media_types',
'label' => 'Export as',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => self::MEDIA_TYPE_OBJECTS,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'call_duration',
'label' => 'Call Duration',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_call_duration',
'max' => 'max_call_duration',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getRecipientsFieldData(?Team $team = null, array $value = []): array
{
return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);
}
public function getJiminnyRecipientsFieldData(array $value = []): array
{
return $this->recipientsService->getJiminnyRecipientsFieldData($value);
}
public function getAdditionalPromptInputFieldData(?string $value = null): array
{
return [
'id' => 'additional_prompt_input',
'label' => 'Special requirements',
'inputType' => InputTypeEnum::TEXTAREA,
'required' => false,
'placeholder' => 'What should be the focus of the report?',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCustomReportNameFieldData(?string $value = null): array
{
return [
'id' => 'custom_name',
'label' => 'Custom report name',
'inputType' => InputTypeEnum::TEXT,
'required' => false,
'placeholder' => 'Enter custom name',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
// data providers
public function getTeams(?Partner $partner = null): array
{
$teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);
$teamData = [];
foreach ($teams as $team) {
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
continue;
}
$teamData[] = $this->transformTeam($team);
}
return $teamData;
}
public function getTeamGroups(string $teamUuid): array
{
$data = [];
$team = $this->getTeam($teamUuid);
if ($team !== null) {
$groups = $team->groups()->get();
foreach ($groups as $group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
];
}
}
return $data;
}
public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array
{
$data = [];
$teams = $this->getTeams(partner: $partner);
foreach ($teams as $team) {
if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {
continue;
}
$data[] = [
'label' => $team['name'],
'groups' => $this->getTeamGroups($team['id']),
];
}
return $data;
}
public function getTeam(string $teamUuid): ?Team
{
return $this->teamRepository->idOrUuid($teamUuid);
}
public function getTeamById(int $teamId): ?Team
{
return $this->teamRepository->find($teamId);
}
public function getGroupsUuids(AutomatedReport $report): array
{
$uuids = [];
$reportGroups = $report->getGroups();
foreach ($reportGroups as $groupId) {
if ($group = $this->groupRepository->find($groupId)) {
$uuids[] = $group->getUuid();
}
}
return $uuids;
}
public function getPlaybookCategoriesUuids(AutomatedReport $report): array
{
$uuids = [];
$playbookCategories = $report->getPlaybookCategories();
foreach ($playbookCategories as $id) {
if ($category = $this->playbookCategoryRepository->find($id)) {
$uuids[] = $category->getUuid();
}
}
return $uuids;
}
public function getDealAtCallStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getDealAtCallStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getCurrentDealStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getCurrentDealStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getRecipients());
}
public function getJiminnyUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function extractUserUuids(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => $user->getUuid())
->values()
->all();
}
// get mail data
public function getRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getRecipients());
}
/**
* @return array<UserContract>
*/
public function getRecipientUserObjects(AutomatedReport $report): array
{
$userIds = $report->getRecipients()['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->values()
->all();
}
private function getJiminnyRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function buildRecipientUsers(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => [
'email' => $user->getEmailAddress(),
'name' => $user->getName(),
'timezone' => $user->getTimezone()->getName(),
])
->values()
->all();
}
public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array
{
if ($report->isAskJiminnyReport()) {
$recipients = $this->resolveAskJiminnyRecipients($report);
} else {
$recipients = $this->getRecipientUsers($report);
if ($includeJiminny) {
$recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));
}
}
$emails = [];
return array_values(array_filter(
$recipients,
static function ($recipient) use (&$emails) {
if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {
return false;
}
$emails[] = $recipient['email'];
return true;
}
));
}
private function resolveAskJiminnyRecipients(AutomatedReport $report): array
{
$recipients = [];
$creator = $report->getCreator();
if ($creator !== null) {
$recipients[] = [
'email' => $creator->getEmailAddress(),
'name' => $creator->getName(),
'timezone' => $creator->getTimezone()->getName(),
];
}
return array_merge(
$recipients,
$this->buildRecipientUsers($report->getRecipients()),
$this->getGroupRecipientUsers($report),
);
}
private function getGroupRecipientUsers(AutomatedReport $report): array
{
$users = [];
foreach ($report->getGroups() as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group === null) {
continue;
}
foreach ($group->getMembers() as $member) {
$users[] = [
'email' => $member->getEmailAddress(),
'name' => $member->getName(),
'timezone' => $member->getTimezone()->getName(),
];
}
}
return $users;
}
public function getReportTypeName(AutomatedReportResult $report): string
{
$type = $report->getReport()->getType();
$getType = $this->transformReportType($type);
return $getType['name'];
}
public function getReportPeriodName(AutomatedReportResult $report): string
{
$from = $report->getFromDate();
$to = $report->getToDate();
$frequency = $report->getReport()->getFrequency();
if ($from === null || $to === null) {
if (! $report->getReport()->isAskJiminnyReport()) {
$invalidPeriod = $from === null ? 'from' : 'to';
throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);
}
$timezone = $report->getReport()->getCreator()?->getTimezone();
$period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);
$from = $period['fromDate'];
$to = $period['toDate'];
}
return $this->formatReportPeriodName($frequency, $from, $to);
}
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 getReportTeamsName(AutomatedReportResult $report): string
{
$groups = $report->getGroups();
if (empty($groups)) {
return 'All';
}
// Get group names from repository
$groupNames = [];
foreach ($groups as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group) {
$groupNames[] = $group->getName();
}
}
if (count($groupNames) === 1) {
// Single team format
$teamsName = $groupNames[0];
} else {
// Multiple teams format
$teamsName = implode(', ', $groupNames);
}
return $teamsName;
}
public function getReportFileName(AutomatedReportResult $report): string
{
$customName = $report->getReport()->getCustomName();
$periodName = $this->getReportPeriodName($report);
$filenameSuffix = $this->getFilenameSuffix($report);
if ($customName) {
if ($filenameSuffix) {
$customName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$customName} - {$periodName}");
}
$baseName = $this->getReportTypeName($report);
if ($filenameSuffix) {
$baseName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}");
}
public function getReportFileNameWithExtension(AutomatedReportResult $result): string
{
$extension = $this->getMediaTypeMetadata($result)['extension'];
return $this->getReportFileName($result) . '.' . $extension;
}
public function sanitizeFileName(string $fileName): string
{
return str_replace(['/', '\\'], '-', $fileName);
}
public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool
{
$recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);
if (in_array($user->getId(), $recipientIds, true)) {
return true;
}
if ($report->isAskJiminnyReport()) {
$groupId = $user->getGroupId();
if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {
return true;
}
}
return false;
}
public function transformReportResults(Collection $automatedReportResults): array
{
$data = [];
foreach ($automatedReportResults as $automatedReportResult) {
/** @var AutomatedReportResult $automatedReportResult */
$report = $automatedReportResult->getReport();
$createdBy = $report->getCreator();
$creator = [
'id' => $createdBy?->getUuid(),
'name' => $createdBy?->getName(),
'email' => $createdBy?->getEmailAddress(),
'photoUrl' => $createdBy?->getPhotoUrl(),
];
$data[] = [
'id' => $automatedReportResult->getUuid(),
'name' => $automatedReportResult->getName(),
'frequency' => $this->transformFrequency($report->getFrequency()),
'recipients' => $this->buildRecipients($report),
'report_type' => $this->transformReportType($report->getType()),
'media_type' => $automatedReportResult->getMediaType(),
'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),
'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),
'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),
'creator' => $creator,
];
}
return $data;
}
private function buildRecipients(AutomatedReport $report): array
{
$creatorUuid = $report->getCreator()?->getUuid();
$recipients = array_values(array_filter(
$this->transformRecipients($report->getRecipients()),
static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,
));
if (! $report->isAskJiminnyReport()) {
return $recipients;
}
return [
...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),
...$recipients,
];
}
public function hasCallTypeConference(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);
}
public function hasCallTypeDialer(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);
}
// transformers
private function transformTeam(Team $team): array
{
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
return [];
}
return [
'id' => $team->getUuid(),
'name' => $team->getName(),
];
}
private function transformReportFullView(AutomatedReport $report): array
{
$base = $this->transformReportBase($report);
return $report->getType() === self::TYPE_ASK_JIMINNY
? $base + $this->transformAskJiminnyFields($report)
: $base + $this->transformStandardReportFields($report);
}
private function transformReportBase(AutomatedReport $report): array
{
return [
'id' => $report->getUuid(),
'organization' => $this->transformOrganization(team: $report->getTeam()),
'report_type' => $this->transformReportType($report->getType()),
'frequency' => $this->transformFrequency($report->getFrequency()),
];
}
private function transformStandardReportFields(AutomatedReport $report): array
{
$team = $report->getTeam();
return [
'report_enabled' => $report->getStatus(),
'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),
'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),
'deal_value_min' => $report->getDealValueMin(),
'deal_value_max' => $report->getDealValueMax(),
'call_types' => $this->transformCallType($report->getCallTypes()),
'media_types' => $this->transformMediaTypes($report),
'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),
'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),
'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),
'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),
'recipients' => $this->transformRecipients($report->getRecipients()),
'created_by' => $this->transformCreator($report->getCreator()),
'additional_prompt_input' => $report->getAdditionalPromptInput(),
'custom_name' => $report->getCustomName(),
'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),
'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),
'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),
];
}
private function transformAskJiminnyFields(AutomatedReport $report): array
{
$team = $report->getTeam();
$creatorId = $report->getAttribute('created_by');
$explicitUserIds = array_values(array_filter(
$report->getRecipients()['users'] ?? [],
static fn ($id) => $id !== $creatorId
));
return [
'report_name' => $report->getCustomName(),
'enabled' => $report->getStatus(),
'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),
'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),
'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),
'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),
];
}
private function transformOrganization(?Team $team): array
{
return [
'id' => $team?->getUuid(),
'name' => $team?->getName(),
];
}
private function transformReportType(string $type): array
{
foreach (self::ALL_TYPES as $typeItem) {
if ($typeItem['id'] === $type) {
return $typeItem;
}
}
return [];
}
private function transformCallType(array $types): array
{
$result = [];
$callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];
foreach ($types as $type) {
foreach ($callTypes as $callTypeItem) {
if ($callTypeItem['id'] === $type) {
$result[] = $callTypeItem;
break;
}
}
}
return $result;
}
private function transformMediaTypes(AutomatedReport $report): array
{
$values = [];
foreach ($report->getMediaTypes() as $mediaType) {
if (! in_array($mediaType, self::MEDIA_TYPES, true)) {
continue;
}
$values[] = match ($mediaType) {
self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,
self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,
};
}
return $values;
}
private function transformFrequency(string $frequency): array
{
foreach (self::ALL_FREQUENCIES as $frequencyItem) {
if ($frequencyItem['id'] === $frequency) {
return $frequencyItem;
}
}
return [];
}
public function transformDurationToMinutes(?int $duration): ?int
{
if (! $duration) {
return null;
}
return (int) ($duration / 60);
}
private function transformGroups(?Team $team, array $groupsIds): array
{
if (empty($groupsIds) || ! $team) {
return [];
}
$data = [];
foreach ($groupsIds as $groupId) {
$group = $team->groups()->where('id', $groupId)->first();
if ($group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
'photoUrl' => $group->getPhotoUrl(),
];
}
}
return $data;
}
private function transformStages(?Team $team, array $stagesIds): array
{
if (empty($stagesIds) || ! $team) {
return [];
}
$data = [];
foreach ($stagesIds as $stageId) {
$stage = $team->stages()->where('id', $stageId)->first();
if ($stage) {
$data[] = [
'id' => $stage->getUuid(),
'name' => $stage->getName(),
];
}
}
return $data;
}
private function transformRecipients(array $recipients): array
{
$users = [];
foreach ($recipients['users'] ?? [] as $userId) {
$users[] = $this->transformUser($userId);
}
return $users;
}
private function transformCreator(?User $user): ?array
{
if ($user === null) {
return null;
}
return $this->transformUser($user->getId());
}
private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array
{
if ($prompt === null) {
return null;
}
return [
'id' => $prompt->getUuid(),
'name' => $prompt->getTitle(),
];
}
private function transformSafeSearch(?Search $search): ?array
{
if ($search === null) {
return null;
}
return [
'id' => $search->getUuid(),
'name' => $search->getName(),
];
}
private function transformUser(int $userId): array
{
/* @var ?User $user */
$user = $this->userRepository->find($userId);
return [
'id' => $user?->getUuid(),
'name' => $user?->getName(),
'email' => $user?->getEmailAddress(),
'photoUrl' => $user?->getPhotoUrl(),
];
}
public function create(array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$validatedData['created_by'] = auth()->id();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
public function update(string $uuid, array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$report = $this->automatedReportsRepository->findByUuid($uuid);
if (! $report) {
throw new InvalidArgumentException('Report not found');
}
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
/**
* Create an Ask Jiminny report.
*/
public function createAskJiminnyReport(array $data, User $creator): array
{
$validatedData = $this->validateAskJiminnyReportData($data, $creator);
$validatedData['created_by'] = $creator->getId();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
return $this->transformReportFullView($automatedReport);
}
/**
* Update an Ask Jiminny report.
*/
public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array
{
if (! $report->isAskJiminnyReport()) {
throw new InvalidArgumentException('Report is not an Ask Jiminny report');
}
$validatedData = $this->validateAskJiminnyReportData($data, $user);
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
return $this->transformReportFullView($automatedReport);
}
public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array
{
if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {
throw new InvalidArgumentException(
'This report is missing a saved search or prompt. ' .
'Edit the report to complete the setup before enabling it.'
);
}
$this->automatedReportsRepository->update($report, ['status' => $status]);
return $this->transformReportFullView($report->fresh());
}
/**
* Validate and transform data for Ask Jiminny reports.
*/
private function validateAskJiminnyReportData(array $data, User $user): array
{
// Validate name
$name = trim($data['report_name'] ?? '');
if (empty($name)) {
throw new InvalidArgumentException('Report name is required');
}
if (mb_strlen($name) > 50) {
throw new InvalidArgumentException('Report name must be 50 characters or less');
}
// Validate frequency (only daily, weekly, monthly for Ask Jiminny)
$frequency = $data['frequency'] ?? null;
$askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];
if (! in_array($frequency, $askJiminnyFrequencies, true)) {
throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');
}
// Validate expiration date
$expiresAt = $data['expires_on'] ?? null;
if (empty($expiresAt)) {
throw new InvalidArgumentException('Expiration date is required');
}
try {
$expiresAtDate = Carbon::parse($expiresAt);
} catch (InvalidFormatException $e) {
throw new InvalidArgumentException('Expiration date format is invalid');
}
$maxExpiration = Carbon::now()->addYear()->endOfDay();
if ($expiresAtDate->gt($maxExpiration)) {
throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');
}
if ($expiresAtDate->isPast()) {
throw new InvalidArgumentException('Expiration date cannot be in the past');
}
// Validate saved search
$activitySearchId = $data['saved_search'] ?? null;
if (empty($activitySearchId)) {
throw new InvalidArgumentException('Saved search is required');
}
$savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);
if (! $savedSearch) {
throw new InvalidArgumentException('Saved search not found or does not belong to you');
}
// Validate saved prompt
$askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;
if (empty($askAnythingPromptId)) {
throw new InvalidArgumentException('Ask Jiminny prompt is required');
}
$prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);
if (! $prompt) {
throw new InvalidArgumentException('Ask Jiminny prompt not found');
}
// Validate status
$status = $data['enabled'] ?? false;
$recipientUserIds = [$user->getId()];
if (! empty($data['share_users'])) {
$sharedUserIds = $this->validateAndGetUserIdsByTeam(
$user->team,
(array) $data['share_users']
);
$recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);
}
$sharedGroupIds = [];
if (! empty($data['share_teams'])) {
$sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);
}
$recipientUserIds = array_values(array_unique($recipientUserIds));
return [
'team_id' => $user->getTeamId(),
'type' => self::TYPE_ASK_JIMINNY,
'status' => (bool) $status,
'frequency' => $frequency,
'custom_name' => $name,
'activity_search_id' => $savedSearch->getId(),
'ask_anything_prompt_id' => $prompt->getId(),
'expires_at' => $expiresAtDate->toDateString(),
'media_types' => [self::MEDIA_TYPE_PDF],
'call_types' => [],
'recipients' => ['users' => $recipientUserIds],
'groups' => $sharedGroupIds,
];
}
public static function getAskJiminnyFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::ASK_JIMINNY_FREQUENCIES);
}
public function getAskJiminnyReportFilters(User $user): array
{
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
return [
[
'id' => 'prompt',
'label' => 'Prompt',
'options' => $prompts,
],
[
'id' => 'saved_search',
'label' => 'Saved Search',
'options' => $savedSearches,
],
];
}
public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array
{
$team = $user->getTeam();
$userTimezone = $user->getTimezone();
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
$teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [
'id' => $group->getUuid(),
'name' => $group->getName(),
])->values()->all();
$shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];
$sharedTeamsValue = [];
$sharedUsersValue = [];
if ($report) {
$sharedTeamsValue = $this->transformGroups($team, $report->getGroups());
$recipientUserIds = $report->getRecipients()['users'] ?? [];
$creatorId = $report->getAttribute('created_by');
$sharedUserIds = array_values(array_filter(
$recipientUserIds,
static fn ($id) => $id !== $creatorId
));
$sharedUsersValue = collect($sharedUserIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (User $u) => [
'id' => $u->getUuid(),
'name' => $u->getName(),
])
->values()
->all();
}
return [
'fields' => [
[
'id' => 'enabled',
'inputType' => InputTypeEnum::TOGGLE,
'label' => '',
'value' => $report?->getStatus() ?? false,
],
[
'id' => 'report_name',
'inputType' => InputTypeEnum::TEXT,
'label' => 'Name',
'placeholder' => 'Enter name',
'required' => true,
'validation' => ['maxLength' => 50],
'value' => $report?->getCustomName() ?? '',
...
|
57993
|
NULL
|
NULL
|
NULL
|
|
58021
|
2042
|
3
|
2026-05-19T11:27:43.805122+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190063805_m1.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskAnythingPromptServiceTest
Run 'AskAnythingPromptServiceTest'
Debug 'AskAnythingPromptServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
100
3
34
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Kiosk\AutomatedReports;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Carbon\Exceptions\InvalidFormatException;
use DateTime;
use DateTimeInterface;
use DateTimeZone;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\ActivitySearch\FilterDefinition\InputTypeEnum;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Partner;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Throwable;
class AutomatedReportsService
{
public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';
public const string TYPE_ASK_JIMINNY = 'ask_jiminny';
/**
* Standard report types (used by kiosk for existing automated reports).
*/
// @TODO this will add filter, however if we need to control feature by FF we need conditional logic
public const array TYPES = [
['id' => 'exec_summary', 'name' => 'Exec Summary'],
['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],
['id' => 'product_feedback', 'name' => 'Product Feedback'],
['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],
// ['id' => 'questions', 'name' => 'Questions'],
// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],
];
public const array ALL_TYPES = [
...self::TYPES,
['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],
];
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';
/**
* Frequencies for standard (non-Ask Jiminny) reports.
*/
public const array FREQUENCIES = [
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
/**
* Frequencies for Ask Jiminny reports.
*/
public const array ASK_JIMINNY_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
];
public const string MEDIA_TYPE_PDF = 'pdf';
public const string MEDIA_TYPE_PODCAST = 'podcast';
public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];
public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];
public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];
public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];
public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];
public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];
public const int SENT_REPORT_AT_HOURS = 5;
public const string PDF_KEY = 'pdf';
public const string AUDIO_KEY = 'audio';
private const array ALL_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
private const string S3_DIR = 'reports';
private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];
private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly GroupRepository $groupRepository,
private readonly UserRepository $userRepository,
private readonly StageRepository $stageRepository,
private readonly DealStagesService $dealStagesService,
private readonly RecipientsService $recipientsService,
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly Webhook $webhookService,
private readonly BusDispatcher $dispatcher,
private readonly ActivityTypeService $activityTypeService,
private readonly PlaybookCategoryRepository $playbookCategoryRepository,
private readonly AskAnythingPromptService $askAnythingPromptService,
private readonly SearchRepository $activitySearchRepository,
private readonly AskAnythingRepository $askAnythingRepository,
) {
}
public static function getTypes(): array
{
$types = self::TYPES;
return array_map(static function ($type) {
return $type['id'];
}, $types);
}
public static function getCallTypes(): array
{
return array_map(static function ($callType) {
return $callType['id'];
}, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);
}
public static function getFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::FREQUENCIES);
}
// front-facing structure
public function getReportEnabledFieldData(bool $value = false): array
{
return [
'id' => 'report_enabled',
'label' => '',
'inputType' => InputTypeEnum::TOGGLE,
'value' => $value,
];
}
// Organizations = Teams
public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array
{
$options = $this->getTeams(partner: $partner);
if ($shortVersion) {
return [
'id' => 'organization',
'label' => 'Organization',
'options' => $options,
];
}
return [
'id' => 'organization',
'label' => 'Organization',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $options,
'value' => $value,
'dependencies' => [
'teams',
'deal_stage_at_call',
'current_deal_stage',
'recipients',
ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,
],
'dependsOn' => [],
];
}
// Teams = Groups
public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array
{
if ($shortVersion) {
return [
'id' => 'teams',
'label' => 'Team',
'options' => $options,
];
}
return [
'id' => 'teams',
'label' => 'Team',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => false,
'placeholder' => 'Select',
'options' => $options,
'value' => $value, // value should be an array of objects {id, name}
'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],
'dependsOn' => [],
];
}
public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array
{
$types = [];
if ($team instanceof Team) {
if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
$types = self::TYPES;
}
if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {
$types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];
}
} else {
$types = self::TYPES;
}
if ($shortVersion) {
return [
'id' => 'report_type',
'label' => 'Report Type',
'options' => $types,
];
}
return [
'id' => 'report_type',
'label' => 'Report Type',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $types,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getFrequencyFieldData(?string $value = null): array
{
return [
'id' => 'frequency',
'label' => 'Frequency',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => self::FREQUENCIES,
'value' => $value,
'dependencies' => ['period'],
'dependsOn' => [],
];
}
public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array
{
return [
'id' => 'period',
'label' => 'Select one-off period',
'inputType' => InputTypeEnum::DATE_RANGE,
'required' => true,
'placeholder' => 'Select',
'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],
'queryParams' => [
'startDate' => 'start_date_period',
'endDate' => 'end_date_period',
],
'dependencies' => [],
'dependsOn' => ['frequency'],
];
}
public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array
{
return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);
}
public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);
}
public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);
}
public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'deal_value',
'label' => 'Deal Value',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_deal_value',
'max' => 'max_deal_value',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array
{
$value = [];
$conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;
$dialerOn && $value[] = self::CALL_TYPE_DIALER;
return [
'id' => 'call_type',
'label' => 'Call Type',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => [
self::CALL_TYPE_CONFERENCE,
self::CALL_TYPE_DIALER,
],
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getMediaTypeFieldData(?AutomatedReport $report = null): array
{
$value = [];
if ($report) {
$value = $this->transformMediaTypes($report);
}
return [
'id' => 'media_types',
'label' => 'Export as',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => self::MEDIA_TYPE_OBJECTS,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'call_duration',
'label' => 'Call Duration',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_call_duration',
'max' => 'max_call_duration',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getRecipientsFieldData(?Team $team = null, array $value = []): array
{
return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);
}
public function getJiminnyRecipientsFieldData(array $value = []): array
{
return $this->recipientsService->getJiminnyRecipientsFieldData($value);
}
public function getAdditionalPromptInputFieldData(?string $value = null): array
{
return [
'id' => 'additional_prompt_input',
'label' => 'Special requirements',
'inputType' => InputTypeEnum::TEXTAREA,
'required' => false,
'placeholder' => 'What should be the focus of the report?',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCustomReportNameFieldData(?string $value = null): array
{
return [
'id' => 'custom_name',
'label' => 'Custom report name',
'inputType' => InputTypeEnum::TEXT,
'required' => false,
'placeholder' => 'Enter custom name',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
// data providers
public function getTeams(?Partner $partner = null): array
{
$teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);
$teamData = [];
foreach ($teams as $team) {
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
continue;
}
$teamData[] = $this->transformTeam($team);
}
return $teamData;
}
public function getTeamGroups(string $teamUuid): array
{
$data = [];
$team = $this->getTeam($teamUuid);
if ($team !== null) {
$groups = $team->groups()->get();
foreach ($groups as $group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
];
}
}
return $data;
}
public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array
{
$data = [];
$teams = $this->getTeams(partner: $partner);
foreach ($teams as $team) {
if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {
continue;
}
$data[] = [
'label' => $team['name'],
'groups' => $this->getTeamGroups($team['id']),
];
}
return $data;
}
public function getTeam(string $teamUuid): ?Team
{
return $this->teamRepository->idOrUuid($teamUuid);
}
public function getTeamById(int $teamId): ?Team
{
return $this->teamRepository->find($teamId);
}
public function getGroupsUuids(AutomatedReport $report): array
{
$uuids = [];
$reportGroups = $report->getGroups();
foreach ($reportGroups as $groupId) {
if ($group = $this->groupRepository->find($groupId)) {
$uuids[] = $group->getUuid();
}
}
return $uuids;
}
public function getPlaybookCategoriesUuids(AutomatedReport $report): array
{
$uuids = [];
$playbookCategories = $report->getPlaybookCategories();
foreach ($playbookCategories as $id) {
if ($category = $this->playbookCategoryRepository->find($id)) {
$uuids[] = $category->getUuid();
}
}
return $uuids;
}
public function getDealAtCallStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getDealAtCallStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getCurrentDealStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getCurrentDealStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getRecipients());
}
public function getJiminnyUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function extractUserUuids(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => $user->getUuid())
->values()
->all();
}
// get mail data
public function getRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getRecipients());
}
/**
* @return array<UserContract>
*/
public function getRecipientUserObjects(AutomatedReport $report): array
{
$userIds = $report->getRecipients()['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->values()
->all();
}
private function getJiminnyRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function buildRecipientUsers(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => [
'email' => $user->getEmailAddress(),
'name' => $user->getName(),
'timezone' => $user->getTimezone()->getName(),
])
->values()
->all();
}
public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array
{
if ($report->isAskJiminnyReport()) {
$recipients = $this->resolveAskJiminnyRecipients($report);
} else {
$recipients = $this->getRecipientUsers($report);
if ($includeJiminny) {
$recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));
}
}
$emails = [];
return array_values(array_filter(
$recipients,
static function ($recipient) use (&$emails) {
if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {
return false;
}
$emails[] = $recipient['email'];
return true;
}
));
}
private function resolveAskJiminnyRecipients(AutomatedReport $report): array
{
$recipients = [];
$creator = $report->getCreator();
if ($creator !== null) {
$recipients[] = [
'email' => $creator->getEmailAddress(),
'name' => $creator->getName(),
'timezone' => $creator->getTimezone()->getName(),
];
}
return array_merge(
$recipients,
$this->buildRecipientUsers($report->getRecipients()),
$this->getGroupRecipientUsers($report),
);
}
private function getGroupRecipientUsers(AutomatedReport $report): array
{
$users = [];
foreach ($report->getGroups() as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group === null) {
continue;
}
foreach ($group->getMembers() as $member) {
$users[] = [
'email' => $member->getEmailAddress(),
'name' => $member->getName(),
'timezone' => $member->getTimezone()->getName(),
];
}
}
return $users;
}
public function getReportTypeName(AutomatedReportResult $report): string
{
$type = $report->getReport()->getType();
$getType = $this->transformReportType($type);
return $getType['name'];
}
public function getReportPeriodName(AutomatedReportResult $report): string
{
$from = $report->getFromDate();
$to = $report->getToDate();
$frequency = $report->getReport()->getFrequency();
if ($from === null || $to === null) {
if (! $report->getReport()->isAskJiminnyReport()) {
$invalidPeriod = $from === null ? 'from' : 'to';
throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);
}
$timezone = $report->getReport()->getCreator()?->getTimezone();
$period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);
$from = $period['fromDate'];
$to = $period['toDate'];
}
return $this->formatReportPeriodName($frequency, $from, $to);
}
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 getReportTeamsName(AutomatedReportResult $report): string
{
$groups = $report->getGroups();
if (empty($groups)) {
return 'All';
}
// Get group names from repository
$groupNames = [];
foreach ($groups as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group) {
$groupNames[] = $group->getName();
}
}
if (count($groupNames) === 1) {
// Single team format
$teamsName = $groupNames[0];
} else {
// Multiple teams format
$teamsName = implode(', ', $groupNames);
}
return $teamsName;
}
public function getReportFileName(AutomatedReportResult $report): string
{
$customName = $report->getReport()->getCustomName();
$periodName = $this->getReportPeriodName($report);
$filenameSuffix = $this->getFilenameSuffix($report);
if ($customName) {
if ($filenameSuffix) {
$customName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$customName} - {$periodName}");
}
$baseName = $this->getReportTypeName($report);
if ($filenameSuffix) {
$baseName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}");
}
public function getReportFileNameWithExtension(AutomatedReportResult $result): string
{
$extension = $this->getMediaTypeMetadata($result)['extension'];
return $this->getReportFileName($result) . '.' . $extension;
}
public function sanitizeFileName(string $fileName): string
{
return str_replace(['/', '\\'], '-', $fileName);
}
public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool
{
$recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);
if (in_array($user->getId(), $recipientIds, true)) {
return true;
}
if ($report->isAskJiminnyReport()) {
$groupId = $user->getGroupId();
if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {
return true;
}
}
return false;
}
public function transformReportResults(Collection $automatedReportResults): array
{
$data = [];
foreach ($automatedReportResults as $automatedReportResult) {
/** @var AutomatedReportResult $automatedReportResult */
$report = $automatedReportResult->getReport();
$createdBy = $report->getCreator();
$creator = [
'id' => $createdBy?->getUuid(),
'name' => $createdBy?->getName(),
'email' => $createdBy?->getEmailAddress(),
'photoUrl' => $createdBy?->getPhotoUrl(),
];
$data[] = [
'id' => $automatedReportResult->getUuid(),
'name' => $automatedReportResult->getName(),
'frequency' => $this->transformFrequency($report->getFrequency()),
'recipients' => $this->buildRecipients($report),
'report_type' => $this->transformReportType($report->getType()),
'media_type' => $automatedReportResult->getMediaType(),
'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),
'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),
'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),
'creator' => $creator,
];
}
return $data;
}
private function buildRecipients(AutomatedReport $report): array
{
$creatorUuid = $report->getCreator()?->getUuid();
$recipients = array_values(array_filter(
$this->transformRecipients($report->getRecipients()),
static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,
));
if (! $report->isAskJiminnyReport()) {
return $recipients;
}
return [
...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),
...$recipients,
];
}
public function hasCallTypeConference(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);
}
public function hasCallTypeDialer(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);
}
// transformers
private function transformTeam(Team $team): array
{
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
return [];
}
return [
'id' => $team->getUuid(),
'name' => $team->getName(),
];
}
private function transformReportFullView(AutomatedReport $report): array
{
$base = $this->transformReportBase($report);
return $report->getType() === self::TYPE_ASK_JIMINNY
? $base + $this->transformAskJiminnyFields($report)
: $base + $this->transformStandardReportFields($report);
}
private function transformReportBase(AutomatedReport $report): array
{
return [
'id' => $report->getUuid(),
'organization' => $this->transformOrganization(team: $report->getTeam()),
'report_type' => $this->transformReportType($report->getType()),
'frequency' => $this->transformFrequency($report->getFrequency()),
];
}
private function transformStandardReportFields(AutomatedReport $report): array
{
$team = $report->getTeam();
return [
'report_enabled' => $report->getStatus(),
'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),
'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),
'deal_value_min' => $report->getDealValueMin(),
'deal_value_max' => $report->getDealValueMax(),
'call_types' => $this->transformCallType($report->getCallTypes()),
'media_types' => $this->transformMediaTypes($report),
'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),
'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),
'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),
'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),
'recipients' => $this->transformRecipients($report->getRecipients()),
'created_by' => $this->transformCreator($report->getCreator()),
'additional_prompt_input' => $report->getAdditionalPromptInput(),
'custom_name' => $report->getCustomName(),
'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),
'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),
'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),
];
}
private function transformAskJiminnyFields(AutomatedReport $report): array
{
$team = $report->getTeam();
$creatorId = $report->getAttribute('created_by');
$explicitUserIds = array_values(array_filter(
$report->getRecipients()['users'] ?? [],
static fn ($id) => $id !== $creatorId
));
return [
'report_name' => $report->getCustomName(),
'enabled' => $report->getStatus(),
'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),
'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),
'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),
'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),
];
}
private function transformOrganization(?Team $team): array
{
return [
'id' => $team?->getUuid(),
'name' => $team?->getName(),
];
}
private function transformReportType(string $type): array
{
foreach (self::ALL_TYPES as $typeItem) {
if ($typeItem['id'] === $type) {
return $typeItem;
}
}
return [];
}
private function transformCallType(array $types): array
{
$result = [];
$callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];
foreach ($types as $type) {
foreach ($callTypes as $callTypeItem) {
if ($callTypeItem['id'] === $type) {
$result[] = $callTypeItem;
break;
}
}
}
return $result;
}
private function transformMediaTypes(AutomatedReport $report): array
{
$values = [];
foreach ($report->getMediaTypes() as $mediaType) {
if (! in_array($mediaType, self::MEDIA_TYPES, true)) {
continue;
}
$values[] = match ($mediaType) {
self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,
self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,
};
}
return $values;
}
private function transformFrequency(string $frequency): array
{
foreach (self::ALL_FREQUENCIES as $frequencyItem) {
if ($frequencyItem['id'] === $frequency) {
return $frequencyItem;
}
}
return [];
}
public function transformDurationToMinutes(?int $duration): ?int
{
if (! $duration) {
return null;
}
return (int) ($duration / 60);
}
private function transformGroups(?Team $team, array $groupsIds): array
{
if (empty($groupsIds) || ! $team) {
return [];
}
$data = [];
foreach ($groupsIds as $groupId) {
$group = $team->groups()->where('id', $groupId)->first();
if ($group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
'photoUrl' => $group->getPhotoUrl(),
];
}
}
return $data;
}
private function transformStages(?Team $team, array $stagesIds): array
{
if (empty($stagesIds) || ! $team) {
return [];
}
$data = [];
foreach ($stagesIds as $stageId) {
$stage = $team->stages()->where('id', $stageId)->first();
if ($stage) {
$data[] = [
'id' => $stage->getUuid(),
'name' => $stage->getName(),
];
}
}
return $data;
}
private function transformRecipients(array $recipients): array
{
$users = [];
foreach ($recipients['users'] ?? [] as $userId) {
$users[] = $this->transformUser($userId);
}
return $users;
}
private function transformCreator(?User $user): ?array
{
if ($user === null) {
return null;
}
return $this->transformUser($user->getId());
}
private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array
{
if ($prompt === null) {
return null;
}
return [
'id' => $prompt->getUuid(),
'name' => $prompt->getTitle(),
];
}
private function transformSafeSearch(?Search $search): ?array
{
if ($search === null) {
return null;
}
return [
'id' => $search->getUuid(),
'name' => $search->getName(),
];
}
private function transformUser(int $userId): array
{
/* @var ?User $user */
$user = $this->userRepository->find($userId);
return [
'id' => $user?->getUuid(),
'name' => $user?->getName(),
'email' => $user?->getEmailAddress(),
'photoUrl' => $user?->getPhotoUrl(),
];
}
public function create(array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$validatedData['created_by'] = auth()->id();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
public function update(string $uuid, array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$report = $this->automatedReportsRepository->findByUuid($uuid);
if (! $report) {
throw new InvalidArgumentException('Report not found');
}
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
/**
* Create an Ask Jiminny report.
*/
public function createAskJiminnyReport(array $data, User $creator): array
{
$validatedData = $this->validateAskJiminnyReportData($data, $creator);
$validatedData['created_by'] = $creator->getId();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
return $this->transformReportFullView($automatedReport);
}
/**
* Update an Ask Jiminny report.
*/
public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array
{
if (! $report->isAskJiminnyReport()) {
throw new InvalidArgumentException('Report is not an Ask Jiminny report');
}
$validatedData = $this->validateAskJiminnyReportData($data, $user);
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
return $this->transformReportFullView($automatedReport);
}
public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array
{
if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {
throw new InvalidArgumentException(
'This report is missing a saved search or prompt. ' .
'Edit the report to complete the setup before enabling it.'
);
}
$this->automatedReportsRepository->update($report, ['status' => $status]);
return $this->transformReportFullView($report->fresh());
}
/**
* Validate and transform data for Ask Jiminny reports.
*/
private function validateAskJiminnyReportData(array $data, User $user): array
{
// Validate name
$name = trim($data['report_name'] ?? '');
if (empty($name)) {
throw new InvalidArgumentException('Report name is required');
}
if (mb_strlen($name) > 50) {
throw new InvalidArgumentException('Report name must be 50 characters or less');
}
// Validate frequency (only daily, weekly, monthly for Ask Jiminny)
$frequency = $data['frequency'] ?? null;
$askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];
if (! in_array($frequency, $askJiminnyFrequencies, true)) {
throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');
}
// Validate expiration date
$expiresAt = $data['expires_on'] ?? null;
if (empty($expiresAt)) {
throw new InvalidArgumentException('Expiration date is required');
}
try {
$expiresAtDate = Carbon::parse($expiresAt);
} catch (InvalidFormatException $e) {
throw new InvalidArgumentException('Expiration date format is invalid');
}
$maxExpiration = Carbon::now()->addYear()->endOfDay();
if ($expiresAtDate->gt($maxExpiration)) {
throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');
}
if ($expiresAtDate->isPast()) {
throw new InvalidArgumentException('Expiration date cannot be in the past');
}
// Validate saved search
$activitySearchId = $data['saved_search'] ?? null;
if (empty($activitySearchId)) {
throw new InvalidArgumentException('Saved search is required');
}
$savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);
if (! $savedSearch) {
throw new InvalidArgumentException('Saved search not found or does not belong to you');
}
// Validate saved prompt
$askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;
if (empty($askAnythingPromptId)) {
throw new InvalidArgumentException('Ask Jiminny prompt is required');
}
$prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);
if (! $prompt) {
throw new InvalidArgumentException('Ask Jiminny prompt not found');
}
// Validate status
$status = $data['enabled'] ?? false;
$recipientUserIds = [$user->getId()];
if (! empty($data['share_users'])) {
$sharedUserIds = $this->validateAndGetUserIdsByTeam(
$user->team,
(array) $data['share_users']
);
$recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);
}
$sharedGroupIds = [];
if (! empty($data['share_teams'])) {
$sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);
}
$recipientUserIds = array_values(array_unique($recipientUserIds));
return [
'team_id' => $user->getTeamId(),
'type' => self::TYPE_ASK_JIMINNY,
'status' => (bool) $status,
'frequency' => $frequency,
'custom_name' => $name,
'activity_search_id' => $savedSearch->getId(),
'ask_anything_prompt_id' => $prompt->getId(),
'expires_at' => $expiresAtDate->toDateString(),
'media_types' => [self::MEDIA_TYPE_PDF],
'call_types' => [],
'recipients' => ['users' => $recipientUserIds],
'groups' => $sharedGroupIds,
];
}
public static function getAskJiminnyFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::ASK_JIMINNY_FREQUENCIES);
}
public function getAskJiminnyReportFilters(User $user): array
{
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
return [
[
'id' => 'prompt',
'label' => 'Prompt',
'options' => $prompts,
],
[
'id' => 'saved_search',
'label' => 'Saved Search',
'options' => $savedSearches,
],
];
}
public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array
{
$team = $user->getTeam();
$userTimezone = $user->getTimezone();
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
$teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [
'id' => $group->getUuid(),
'name' => $group->getName(),
])->values()->all();
$shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];
$sharedTeamsValue = [];
$sharedUsersValue = [];
if ($report) {
$sharedTeamsValue = $this->transformGroups($team, $report->getGroups());
$recipientUserIds = $report->getRecipients()['users'] ?? [];
$creatorId = $report->getAttribute('created_by');
$sharedUserIds = array_values(array_filter(
$recipientUserIds,
static fn ($id) => $id !== $creatorId
));
$sharedUsersValue = collect($sharedUserIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (User $u) => [
'id' => $u->getUuid(),
'name' => $u->getName(),
])
->values()
->all();
}
return [
'fields' => [
[
'id' => 'enabled',
'inputType' => InputTypeEnum::TOGGLE,
'label' => '',
'value' => $report?->getStatus() ?? false,
],
[
'id' => 'report_name',
'inputType' => InputTypeEnum::TEXT,
'label' => 'Name',
'placeholder' => 'Enter name',
'required' => true,
'validation' => ['maxLength' => 50],
'value' => $report?->getCustomName() ?? '',
...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects<br/>Some incoming commits are not fetched<br/>","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,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskAnythingPromptServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskAnythingPromptServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskAnythingPromptServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"100","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"34","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"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\\Services\\Kiosk\\AutomatedReports;\n\nuse Carbon\\CarbonImmutable;\nuse Carbon\\CarbonInterface;\nuse Carbon\\Exceptions\\InvalidFormatException;\nuse DateTime;\nuse DateTimeInterface;\nuse DateTimeZone;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\InputTypeEnum;\nuse Jiminny\\Component\\AskAnything\\AskAnythingPromptService;\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Contracts\\Repositories\\PlaybookCategoryRepository;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\ModelNotFoundException;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Partner;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Repositories\\GroupRepository;\nuse Jiminny\\Repositories\\SearchRepository;\nuse Jiminny\\Repositories\\StageRepository;\nuse Throwable;\n\nclass AutomatedReportsService\n{\n public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';\n public const string TYPE_ASK_JIMINNY = 'ask_jiminny';\n\n /**\n * Standard report types (used by kiosk for existing automated reports).\n */\n // @TODO this will add filter, however if we need to control feature by FF we need conditional logic\n public const array TYPES = [\n ['id' => 'exec_summary', 'name' => 'Exec Summary'],\n ['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],\n ['id' => 'product_feedback', 'name' => 'Product Feedback'],\n ['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],\n// ['id' => 'questions', 'name' => 'Questions'],\n// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],\n ];\n\n public const array ALL_TYPES = [\n ...self::TYPES,\n ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],\n ];\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\n /**\n * Frequencies for standard (non-Ask Jiminny) reports.\n */\n public const array FREQUENCIES = [\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n\n /**\n * Frequencies for Ask Jiminny reports.\n */\n public const array ASK_JIMINNY_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ];\n\n public const string MEDIA_TYPE_PDF = 'pdf';\n public const string MEDIA_TYPE_PODCAST = 'podcast';\n public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];\n public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];\n public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];\n public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];\n\n public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];\n public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];\n public const int SENT_REPORT_AT_HOURS = 5;\n public const string PDF_KEY = 'pdf';\n public const string AUDIO_KEY = 'audio';\n\n private const array ALL_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n private const string S3_DIR = 'reports';\n private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];\n private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];\n\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly GroupRepository $groupRepository,\n private readonly UserRepository $userRepository,\n private readonly StageRepository $stageRepository,\n private readonly DealStagesService $dealStagesService,\n private readonly RecipientsService $recipientsService,\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly Webhook $webhookService,\n private readonly BusDispatcher $dispatcher,\n private readonly ActivityTypeService $activityTypeService,\n private readonly PlaybookCategoryRepository $playbookCategoryRepository,\n private readonly AskAnythingPromptService $askAnythingPromptService,\n private readonly SearchRepository $activitySearchRepository,\n private readonly AskAnythingRepository $askAnythingRepository,\n ) {\n }\n\n public static function getTypes(): array\n {\n $types = self::TYPES;\n\n return array_map(static function ($type) {\n return $type['id'];\n }, $types);\n }\n\n public static function getCallTypes(): array\n {\n return array_map(static function ($callType) {\n return $callType['id'];\n }, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);\n }\n\n public static function getFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::FREQUENCIES);\n }\n\n // front-facing structure\n public function getReportEnabledFieldData(bool $value = false): array\n {\n return [\n 'id' => 'report_enabled',\n 'label' => '',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'value' => $value,\n ];\n }\n\n // Organizations = Teams\n public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array\n {\n $options = $this->getTeams(partner: $partner);\n\n if ($shortVersion) {\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value,\n 'dependencies' => [\n 'teams',\n 'deal_stage_at_call',\n 'current_deal_stage',\n 'recipients',\n ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,\n ],\n 'dependsOn' => [],\n ];\n }\n\n // Teams = Groups\n public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array\n {\n if ($shortVersion) {\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value, // value should be an array of objects {id, name}\n 'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],\n 'dependsOn' => [],\n ];\n }\n\n public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array\n {\n $types = [];\n if ($team instanceof Team) {\n if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n $types = self::TYPES;\n }\n if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {\n $types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];\n }\n } else {\n $types = self::TYPES;\n }\n\n if ($shortVersion) {\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'options' => $types,\n ];\n }\n\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $types,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getFrequencyFieldData(?string $value = null): array\n {\n return [\n 'id' => 'frequency',\n 'label' => 'Frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::FREQUENCIES,\n 'value' => $value,\n 'dependencies' => ['period'],\n 'dependsOn' => [],\n ];\n }\n\n public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array\n {\n return [\n 'id' => 'period',\n 'label' => 'Select one-off period',\n 'inputType' => InputTypeEnum::DATE_RANGE,\n 'required' => true,\n 'placeholder' => 'Select',\n 'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],\n 'queryParams' => [\n 'startDate' => 'start_date_period',\n 'endDate' => 'end_date_period',\n ],\n 'dependencies' => [],\n 'dependsOn' => ['frequency'],\n ];\n }\n\n public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array\n {\n return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);\n }\n\n public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);\n }\n\n public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);\n }\n\n public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'deal_value',\n 'label' => 'Deal Value',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_deal_value',\n 'max' => 'max_deal_value',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array\n {\n $value = [];\n $conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;\n $dialerOn && $value[] = self::CALL_TYPE_DIALER;\n\n return [\n 'id' => 'call_type',\n 'label' => 'Call Type',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => [\n self::CALL_TYPE_CONFERENCE,\n self::CALL_TYPE_DIALER,\n ],\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getMediaTypeFieldData(?AutomatedReport $report = null): array\n {\n $value = [];\n\n if ($report) {\n $value = $this->transformMediaTypes($report);\n }\n\n return [\n 'id' => 'media_types',\n 'label' => 'Export as',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => self::MEDIA_TYPE_OBJECTS,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'call_duration',\n 'label' => 'Call Duration',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_call_duration',\n 'max' => 'max_call_duration',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getRecipientsFieldData(?Team $team = null, array $value = []): array\n {\n return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);\n }\n\n public function getJiminnyRecipientsFieldData(array $value = []): array\n {\n return $this->recipientsService->getJiminnyRecipientsFieldData($value);\n }\n\n public function getAdditionalPromptInputFieldData(?string $value = null): array\n {\n return [\n 'id' => 'additional_prompt_input',\n 'label' => 'Special requirements',\n 'inputType' => InputTypeEnum::TEXTAREA,\n 'required' => false,\n 'placeholder' => 'What should be the focus of the report?',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCustomReportNameFieldData(?string $value = null): array\n {\n return [\n 'id' => 'custom_name',\n 'label' => 'Custom report name',\n 'inputType' => InputTypeEnum::TEXT,\n 'required' => false,\n 'placeholder' => 'Enter custom name',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n // data providers\n public function getTeams(?Partner $partner = null): array\n {\n $teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);\n\n $teamData = [];\n foreach ($teams as $team) {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n continue;\n }\n\n $teamData[] = $this->transformTeam($team);\n }\n\n return $teamData;\n }\n\n public function getTeamGroups(string $teamUuid): array\n {\n $data = [];\n $team = $this->getTeam($teamUuid);\n\n if ($team !== null) {\n $groups = $team->groups()->get();\n\n foreach ($groups as $group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array\n {\n $data = [];\n $teams = $this->getTeams(partner: $partner);\n\n foreach ($teams as $team) {\n if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {\n continue;\n }\n\n $data[] = [\n 'label' => $team['name'],\n 'groups' => $this->getTeamGroups($team['id']),\n ];\n }\n\n return $data;\n }\n\n public function getTeam(string $teamUuid): ?Team\n {\n return $this->teamRepository->idOrUuid($teamUuid);\n }\n\n public function getTeamById(int $teamId): ?Team\n {\n return $this->teamRepository->find($teamId);\n }\n\n public function getGroupsUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportGroups = $report->getGroups();\n foreach ($reportGroups as $groupId) {\n if ($group = $this->groupRepository->find($groupId)) {\n $uuids[] = $group->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getPlaybookCategoriesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $playbookCategories = $report->getPlaybookCategories();\n foreach ($playbookCategories as $id) {\n if ($category = $this->playbookCategoryRepository->find($id)) {\n $uuids[] = $category->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getDealAtCallStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getDealAtCallStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getCurrentDealStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getCurrentDealStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getRecipients());\n }\n\n public function getJiminnyUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function extractUserUuids(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => $user->getUuid())\n ->values()\n ->all();\n }\n\n // get mail data\n public function getRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getRecipients());\n }\n\n /**\n * @return array<UserContract>\n */\n public function getRecipientUserObjects(AutomatedReport $report): array\n {\n $userIds = $report->getRecipients()['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->values()\n ->all();\n }\n\n private function getJiminnyRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function buildRecipientUsers(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => [\n 'email' => $user->getEmailAddress(),\n 'name' => $user->getName(),\n 'timezone' => $user->getTimezone()->getName(),\n ])\n ->values()\n ->all();\n }\n\n public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array\n {\n if ($report->isAskJiminnyReport()) {\n $recipients = $this->resolveAskJiminnyRecipients($report);\n } else {\n $recipients = $this->getRecipientUsers($report);\n if ($includeJiminny) {\n $recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));\n }\n }\n\n $emails = [];\n\n return array_values(array_filter(\n $recipients,\n static function ($recipient) use (&$emails) {\n if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {\n return false;\n }\n $emails[] = $recipient['email'];\n\n return true;\n }\n ));\n }\n\n private function resolveAskJiminnyRecipients(AutomatedReport $report): array\n {\n $recipients = [];\n\n $creator = $report->getCreator();\n if ($creator !== null) {\n $recipients[] = [\n 'email' => $creator->getEmailAddress(),\n 'name' => $creator->getName(),\n 'timezone' => $creator->getTimezone()->getName(),\n ];\n }\n\n return array_merge(\n $recipients,\n $this->buildRecipientUsers($report->getRecipients()),\n $this->getGroupRecipientUsers($report),\n );\n }\n\n private function getGroupRecipientUsers(AutomatedReport $report): array\n {\n $users = [];\n foreach ($report->getGroups() as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group === null) {\n continue;\n }\n foreach ($group->getMembers() as $member) {\n $users[] = [\n 'email' => $member->getEmailAddress(),\n 'name' => $member->getName(),\n 'timezone' => $member->getTimezone()->getName(),\n ];\n }\n }\n\n return $users;\n }\n\n public function getReportTypeName(AutomatedReportResult $report): string\n {\n $type = $report->getReport()->getType();\n\n $getType = $this->transformReportType($type);\n\n return $getType['name'];\n }\n\n public function getReportPeriodName(AutomatedReportResult $report): string\n {\n $from = $report->getFromDate();\n $to = $report->getToDate();\n $frequency = $report->getReport()->getFrequency();\n\n if ($from === null || $to === null) {\n if (! $report->getReport()->isAskJiminnyReport()) {\n $invalidPeriod = $from === null ? 'from' : 'to';\n\n throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);\n }\n\n $timezone = $report->getReport()->getCreator()?->getTimezone();\n $period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);\n $from = $period['fromDate'];\n $to = $period['toDate'];\n }\n\n return $this->formatReportPeriodName($frequency, $from, $to);\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 getReportTeamsName(AutomatedReportResult $report): string\n {\n $groups = $report->getGroups();\n\n if (empty($groups)) {\n return 'All';\n }\n\n // Get group names from repository\n $groupNames = [];\n foreach ($groups as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group) {\n $groupNames[] = $group->getName();\n }\n }\n\n if (count($groupNames) === 1) {\n // Single team format\n $teamsName = $groupNames[0];\n } else {\n // Multiple teams format\n $teamsName = implode(', ', $groupNames);\n }\n\n return $teamsName;\n }\n\n public function getReportFileName(AutomatedReportResult $report): string\n {\n $customName = $report->getReport()->getCustomName();\n $periodName = $this->getReportPeriodName($report);\n $filenameSuffix = $this->getFilenameSuffix($report);\n\n if ($customName) {\n if ($filenameSuffix) {\n $customName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$customName} - {$periodName}\");\n }\n\n $baseName = $this->getReportTypeName($report);\n\n if ($filenameSuffix) {\n $baseName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}\");\n }\n\n public function getReportFileNameWithExtension(AutomatedReportResult $result): string\n {\n $extension = $this->getMediaTypeMetadata($result)['extension'];\n\n return $this->getReportFileName($result) . '.' . $extension;\n }\n\n public function sanitizeFileName(string $fileName): string\n {\n return str_replace(['/', '\\\\'], '-', $fileName);\n }\n\n public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool\n {\n $recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);\n if (in_array($user->getId(), $recipientIds, true)) {\n return true;\n }\n\n if ($report->isAskJiminnyReport()) {\n $groupId = $user->getGroupId();\n if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {\n return true;\n }\n }\n\n return false;\n }\n\n public function transformReportResults(Collection $automatedReportResults): array\n {\n $data = [];\n foreach ($automatedReportResults as $automatedReportResult) {\n /** @var AutomatedReportResult $automatedReportResult */\n\n $report = $automatedReportResult->getReport();\n\n $createdBy = $report->getCreator();\n $creator = [\n 'id' => $createdBy?->getUuid(),\n 'name' => $createdBy?->getName(),\n 'email' => $createdBy?->getEmailAddress(),\n 'photoUrl' => $createdBy?->getPhotoUrl(),\n ];\n\n $data[] = [\n 'id' => $automatedReportResult->getUuid(),\n 'name' => $automatedReportResult->getName(),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n 'recipients' => $this->buildRecipients($report),\n 'report_type' => $this->transformReportType($report->getType()),\n 'media_type' => $automatedReportResult->getMediaType(),\n 'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),\n 'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),\n 'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),\n 'creator' => $creator,\n ];\n }\n\n return $data;\n }\n\n private function buildRecipients(AutomatedReport $report): array\n {\n $creatorUuid = $report->getCreator()?->getUuid();\n\n $recipients = array_values(array_filter(\n $this->transformRecipients($report->getRecipients()),\n static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,\n ));\n\n if (! $report->isAskJiminnyReport()) {\n return $recipients;\n }\n\n return [\n ...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),\n ...$recipients,\n ];\n }\n\n public function hasCallTypeConference(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);\n }\n\n public function hasCallTypeDialer(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);\n }\n\n // transformers\n private function transformTeam(Team $team): array\n {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n return [];\n }\n\n return [\n 'id' => $team->getUuid(),\n 'name' => $team->getName(),\n ];\n }\n\n private function transformReportFullView(AutomatedReport $report): array\n {\n $base = $this->transformReportBase($report);\n\n return $report->getType() === self::TYPE_ASK_JIMINNY\n ? $base + $this->transformAskJiminnyFields($report)\n : $base + $this->transformStandardReportFields($report);\n }\n\n private function transformReportBase(AutomatedReport $report): array\n {\n return [\n 'id' => $report->getUuid(),\n 'organization' => $this->transformOrganization(team: $report->getTeam()),\n 'report_type' => $this->transformReportType($report->getType()),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n ];\n }\n\n private function transformStandardReportFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n\n return [\n 'report_enabled' => $report->getStatus(),\n 'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),\n 'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),\n 'deal_value_min' => $report->getDealValueMin(),\n 'deal_value_max' => $report->getDealValueMax(),\n 'call_types' => $this->transformCallType($report->getCallTypes()),\n 'media_types' => $this->transformMediaTypes($report),\n 'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),\n 'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),\n 'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),\n 'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),\n 'recipients' => $this->transformRecipients($report->getRecipients()),\n 'created_by' => $this->transformCreator($report->getCreator()),\n 'additional_prompt_input' => $report->getAdditionalPromptInput(),\n 'custom_name' => $report->getCustomName(),\n 'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),\n 'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),\n 'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),\n ];\n }\n\n private function transformAskJiminnyFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n $creatorId = $report->getAttribute('created_by');\n $explicitUserIds = array_values(array_filter(\n $report->getRecipients()['users'] ?? [],\n static fn ($id) => $id !== $creatorId\n ));\n\n return [\n 'report_name' => $report->getCustomName(),\n 'enabled' => $report->getStatus(),\n 'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),\n 'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),\n 'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),\n 'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),\n ];\n }\n\n private function transformOrganization(?Team $team): array\n {\n return [\n 'id' => $team?->getUuid(),\n 'name' => $team?->getName(),\n ];\n }\n\n private function transformReportType(string $type): array\n {\n foreach (self::ALL_TYPES as $typeItem) {\n if ($typeItem['id'] === $type) {\n return $typeItem;\n }\n }\n\n return [];\n }\n\n private function transformCallType(array $types): array\n {\n $result = [];\n $callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];\n\n foreach ($types as $type) {\n foreach ($callTypes as $callTypeItem) {\n if ($callTypeItem['id'] === $type) {\n $result[] = $callTypeItem;\n\n break;\n }\n }\n }\n\n return $result;\n }\n\n private function transformMediaTypes(AutomatedReport $report): array\n {\n $values = [];\n\n foreach ($report->getMediaTypes() as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n continue;\n }\n\n $values[] = match ($mediaType) {\n self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,\n self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,\n };\n }\n\n return $values;\n }\n\n private function transformFrequency(string $frequency): array\n {\n foreach (self::ALL_FREQUENCIES as $frequencyItem) {\n if ($frequencyItem['id'] === $frequency) {\n return $frequencyItem;\n }\n }\n\n return [];\n }\n\n public function transformDurationToMinutes(?int $duration): ?int\n {\n if (! $duration) {\n return null;\n }\n\n return (int) ($duration / 60);\n }\n\n private function transformGroups(?Team $team, array $groupsIds): array\n {\n if (empty($groupsIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($groupsIds as $groupId) {\n $group = $team->groups()->where('id', $groupId)->first();\n\n if ($group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n 'photoUrl' => $group->getPhotoUrl(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformStages(?Team $team, array $stagesIds): array\n {\n if (empty($stagesIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($stagesIds as $stageId) {\n $stage = $team->stages()->where('id', $stageId)->first();\n\n if ($stage) {\n $data[] = [\n 'id' => $stage->getUuid(),\n 'name' => $stage->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformRecipients(array $recipients): array\n {\n $users = [];\n foreach ($recipients['users'] ?? [] as $userId) {\n $users[] = $this->transformUser($userId);\n }\n\n return $users;\n }\n\n private function transformCreator(?User $user): ?array\n {\n if ($user === null) {\n return null;\n }\n\n return $this->transformUser($user->getId());\n }\n\n private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array\n {\n if ($prompt === null) {\n return null;\n }\n\n return [\n 'id' => $prompt->getUuid(),\n 'name' => $prompt->getTitle(),\n ];\n }\n\n private function transformSafeSearch(?Search $search): ?array\n {\n if ($search === null) {\n return null;\n }\n\n return [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ];\n }\n\n private function transformUser(int $userId): array\n {\n /* @var ?User $user */\n $user = $this->userRepository->find($userId);\n\n return [\n 'id' => $user?->getUuid(),\n 'name' => $user?->getName(),\n 'email' => $user?->getEmailAddress(),\n 'photoUrl' => $user?->getPhotoUrl(),\n ];\n }\n\n public function create(array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $validatedData['created_by'] = auth()->id();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function update(string $uuid, array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $report = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $report) {\n throw new InvalidArgumentException('Report not found');\n }\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Create an Ask Jiminny report.\n */\n public function createAskJiminnyReport(array $data, User $creator): array\n {\n $validatedData = $this->validateAskJiminnyReportData($data, $creator);\n $validatedData['created_by'] = $creator->getId();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Update an Ask Jiminny report.\n */\n public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array\n {\n if (! $report->isAskJiminnyReport()) {\n throw new InvalidArgumentException('Report is not an Ask Jiminny report');\n }\n\n $validatedData = $this->validateAskJiminnyReportData($data, $user);\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array\n {\n if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {\n throw new InvalidArgumentException(\n 'This report is missing a saved search or prompt. ' .\n 'Edit the report to complete the setup before enabling it.'\n );\n }\n\n $this->automatedReportsRepository->update($report, ['status' => $status]);\n\n return $this->transformReportFullView($report->fresh());\n }\n\n /**\n * Validate and transform data for Ask Jiminny reports.\n */\n private function validateAskJiminnyReportData(array $data, User $user): array\n {\n // Validate name\n $name = trim($data['report_name'] ?? '');\n if (empty($name)) {\n throw new InvalidArgumentException('Report name is required');\n }\n if (mb_strlen($name) > 50) {\n throw new InvalidArgumentException('Report name must be 50 characters or less');\n }\n\n // Validate frequency (only daily, weekly, monthly for Ask Jiminny)\n $frequency = $data['frequency'] ?? null;\n $askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];\n if (! in_array($frequency, $askJiminnyFrequencies, true)) {\n throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');\n }\n\n // Validate expiration date\n $expiresAt = $data['expires_on'] ?? null;\n if (empty($expiresAt)) {\n throw new InvalidArgumentException('Expiration date is required');\n }\n\n try {\n $expiresAtDate = Carbon::parse($expiresAt);\n } catch (InvalidFormatException $e) {\n throw new InvalidArgumentException('Expiration date format is invalid');\n }\n $maxExpiration = Carbon::now()->addYear()->endOfDay();\n if ($expiresAtDate->gt($maxExpiration)) {\n throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');\n }\n if ($expiresAtDate->isPast()) {\n throw new InvalidArgumentException('Expiration date cannot be in the past');\n }\n\n // Validate saved search\n $activitySearchId = $data['saved_search'] ?? null;\n if (empty($activitySearchId)) {\n throw new InvalidArgumentException('Saved search is required');\n }\n $savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);\n if (! $savedSearch) {\n throw new InvalidArgumentException('Saved search not found or does not belong to you');\n }\n\n // Validate saved prompt\n $askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;\n if (empty($askAnythingPromptId)) {\n throw new InvalidArgumentException('Ask Jiminny prompt is required');\n }\n $prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);\n if (! $prompt) {\n throw new InvalidArgumentException('Ask Jiminny prompt not found');\n }\n\n // Validate status\n $status = $data['enabled'] ?? false;\n\n $recipientUserIds = [$user->getId()];\n\n if (! empty($data['share_users'])) {\n $sharedUserIds = $this->validateAndGetUserIdsByTeam(\n $user->team,\n (array) $data['share_users']\n );\n $recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);\n }\n\n $sharedGroupIds = [];\n if (! empty($data['share_teams'])) {\n $sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);\n }\n\n $recipientUserIds = array_values(array_unique($recipientUserIds));\n\n return [\n 'team_id' => $user->getTeamId(),\n 'type' => self::TYPE_ASK_JIMINNY,\n 'status' => (bool) $status,\n 'frequency' => $frequency,\n 'custom_name' => $name,\n 'activity_search_id' => $savedSearch->getId(),\n 'ask_anything_prompt_id' => $prompt->getId(),\n 'expires_at' => $expiresAtDate->toDateString(),\n 'media_types' => [self::MEDIA_TYPE_PDF],\n 'call_types' => [],\n 'recipients' => ['users' => $recipientUserIds],\n 'groups' => $sharedGroupIds,\n ];\n }\n\n public static function getAskJiminnyFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::ASK_JIMINNY_FREQUENCIES);\n }\n\n public function getAskJiminnyReportFilters(User $user): array\n {\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n return [\n [\n 'id' => 'prompt',\n 'label' => 'Prompt',\n 'options' => $prompts,\n ],\n [\n 'id' => 'saved_search',\n 'label' => 'Saved Search',\n 'options' => $savedSearches,\n ],\n ];\n }\n\n public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array\n {\n $team = $user->getTeam();\n $userTimezone = $user->getTimezone();\n\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n $teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ])->values()->all();\n\n $shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];\n\n $sharedTeamsValue = [];\n $sharedUsersValue = [];\n if ($report) {\n $sharedTeamsValue = $this->transformGroups($team, $report->getGroups());\n\n $recipientUserIds = $report->getRecipients()['users'] ?? [];\n $creatorId = $report->getAttribute('created_by');\n $sharedUserIds = array_values(array_filter(\n $recipientUserIds,\n static fn ($id) => $id !== $creatorId\n ));\n $sharedUsersValue = collect($sharedUserIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (User $u) => [\n 'id' => $u->getUuid(),\n 'name' => $u->getName(),\n ])\n ->values()\n ->all();\n }\n\n return [\n 'fields' => [\n [\n 'id' => 'enabled',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'label' => '',\n 'value' => $report?->getStatus() ?? false,\n ],\n [\n 'id' => 'report_name',\n 'inputType' => InputTypeEnum::TEXT,\n 'label' => 'Name',\n 'placeholder' => 'Enter name',\n 'required' => true,\n 'validation' => ['maxLength' => 50],\n 'value' => $report?->getCustomName() ?? '',\n ],\n [\n 'id' => 'frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Frequency',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::ASK_JIMINNY_FREQUENCIES,\n 'value' => $report ? $this->transformFrequency($report->getFrequency()) : null,\n ],\n [\n 'id' => 'expires_on',\n 'inputType' => InputTypeEnum::DATE,\n 'label' => 'Expires on',\n 'required' => true,\n 'placeholder' => 'Select',\n 'validation' => [\n 'minDate' => now($userTimezone)->toDateString(),\n 'maxDate' => now($userTimezone)->addYear()->toDateString(),\n ],\n 'value' => $report?->getExpiresAt()?->toDateString(),\n ],\n [\n 'id' => 'share_teams',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team',\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $teamGroups,\n 'value' => $sharedTeamsValue,\n ],\n [\n 'id' => 'share_users',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team member',\n 'required' => false,\n 'placeholder' => 'Select',\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'options' => $shareUsers,\n 'value' => $sharedUsersValue,\n ],\n [\n 'id' => 'saved_search',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Saved search',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $savedSearches,\n 'value' => $report && $report->getSavedSearch() ? [\n 'id' => $report->getSavedSearch()->getUuid(),\n 'name' => $report->getSavedSearch()->getName(),\n ] : null,\n ],\n [\n 'id' => 'ask_jiminny_prompt',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Ask Jiminny prompt',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $prompts,\n 'value' => $report && $report->getAskAnythingPrompt() ? [\n 'id' => $report->getAskAnythingPrompt()->getUuid(),\n 'name' => $report->getAskAnythingPrompt()->getTitle(),\n ] : null,\n ],\n ],\n ];\n }\n\n private function updateResultNames(AutomatedReport $automatedReport): void\n {\n $results = $this->automatedReportsRepository->getResultsByReport($automatedReport);\n\n foreach ($results as $result) {\n $result->update(['name' => $this->getReportFileName($result)]);\n }\n }\n\n public function updateStatus(string $uuid, array $data): array\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $automatedReport->update([\n 'status' => $status,\n ]);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n private function generateOneOffReport(AutomatedReport $automatedReport): void\n {\n // the scheduler handles all the other frequency types\n if ($automatedReport->getStatus() === false || $automatedReport->getFrequency() !== self::FREQUENCY_ONE_OFF) {\n return;\n }\n\n $this->dispatcher->dispatch(new RequestGenerateReportJob($automatedReport->getUuid()));\n }\n\n public function getReport(string $uuid, ?Partner $partner = null): AutomatedReport\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n if ($partner !== null && ! $partner->isDefaultPartner() && $automatedReport->team->partner_id !== $partner->getId()) {\n throw new ModelNotFoundException('Report not found');\n }\n\n return $automatedReport;\n }\n\n public function get(string $uuid, ?Partner $partner = null): array\n {\n $automatedReport = $this->getReport($uuid, $partner);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function list(string $sortColumn = 'created_at', string $sortDirection = 'desc', ?Partner $partner = null): array\n {\n $results = [];\n $collection = $this->automatedReportsRepository->getAllStandardReports($sortColumn, $sortDirection, $partner);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function listAskJiminnyReports(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): array {\n $results = [];\n $collection = $this->automatedReportsRepository->getAskJiminnyReportsByUser($user, $sortColumn, $sortDirection);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function delete(string $uuid): void\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $automatedReport->delete();\n }\n\n public function createReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n return $this->automatedReportsRepository->createResult(\n array_merge(\n [\n 'report_id' => $automatedReport->getId(),\n 'status' => AutomatedReportResult::STATUS_DEFAULT,\n ],\n $data\n )\n );\n }\n\n public function getOrCreateReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n $existing = $this->automatedReportsRepository->findLatestSameDayDefaultOrFailedResult($automatedReport);\n\n if ($existing !== null) {\n $existing->update(['status' => AutomatedReportResult::STATUS_DEFAULT]);\n\n return $existing;\n }\n\n return $this->createReportResult($automatedReport, $data);\n }\n\n public function getReportResult(string $resultUuid): AutomatedReportResult\n {\n $report = $this->automatedReportsRepository->findResultByUuid($resultUuid);\n\n if (! $report) {\n throw new ModelNotFoundException('Report Result not found');\n }\n\n return $report;\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return $this->automatedReportsRepository->findChildResult($result, $type);\n }\n\n // prophet API calls\n /**\n * @throws ApplicationException\n */\n public function getGenerateReportPayload(AutomatedReport $automatedReport, string $reportResultUuid): array\n {\n $period = $this->calculateFromAndToDate($automatedReport);\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n return [\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResultUuid,\n 'report_type' => $automatedReport->getType(),\n 'media_types' => $automatedReport->getMediaTypes(),\n 'from_date' => $fromDate->startOfDay()->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->endOfDay()->format(DateTimeInterface::RFC3339),\n 'group_ids' => $automatedReport->getGroups(),\n 'call_deal_stage' => $automatedReport->getDealAtCallStages(),\n 'current_deal_stage' => $automatedReport->getCurrentDealStages(),\n 'deal_min_value' => $automatedReport->getDealValueMin(),\n 'deal_max_value' => $automatedReport->getDealValueMax(),\n 'call_types' => $automatedReport->getCallTypes(),\n 'call_duration_min_seconds' => $automatedReport->getCallDurationMin(),\n 'call_duration_max_seconds' => $automatedReport->getCallDurationMax(),\n 'special_requirements' => $automatedReport->getAdditionalPromptInput(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->formatReportPeriodName(\n $automatedReport->getFrequency(),\n $fromDate,\n $toDate,\n ),\n 'playbook_categories' => $automatedReport->getPlaybookCategories(),\n 'custom_name' => $automatedReport->getCustomName(),\n ];\n }\n\n // $inputPayload - FE payload structure\n public function getActivitiesCountPayload(array $inputPayload): array\n {\n // Use validateAndTransformData to validate and normalize input\n $validatedData = $this->validateAndTransformData($inputPayload);\n $period = $this->calculateFromAndToDatePeriod(\n $validatedData['frequency'],\n Carbon::parse($validatedData['from']),\n Carbon::parse($validatedData['to']),\n );\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n // Create payload similar to getGenerateReportPayload\n return [\n 'team_id' => $validatedData['team_id'],\n 'group_ids' => $validatedData['groups'] ?? [],\n 'report_type' => $validatedData['type'],\n 'from_date' => $fromDate->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->format(DateTimeInterface::RFC3339),\n 'call_deal_stage' => $validatedData['deal_at_call_stages'] ?? [],\n 'current_deal_stage' => $validatedData['current_deal_stages'] ?? [],\n 'deal_min_value' => $validatedData['deal_value_min'] ?? null,\n 'deal_max_value' => $validatedData['deal_value_max'] ?? null,\n 'call_types' => $validatedData['call_types'],\n 'call_duration_min_seconds' => $validatedData['call_duration_min'] ?? null,\n 'call_duration_max_seconds' => $validatedData['call_duration_max'] ?? null,\n 'special_requirements' => $validatedData['additional_prompt_input'] ?? null,\n 'playbook_categories' => $validatedData['playbook_categories'] ?? [],\n 'request_id' => null,\n 'callback_url' => null,\n ];\n }\n\n public function shouldSendReport(array $users, ?CarbonInterface $generatedAt = null): bool\n {\n if (empty($users)) {\n return false;\n }\n\n $earliestTz = collect($users)\n ->mapWithKeys(function (array $user) {\n $tz = new DateTimeZone($user['timezone']);\n $nowUtc = new DateTime('now', new DateTimeZone('UTC'));\n $offset = $tz->getOffset($nowUtc);\n\n return [$user['timezone'] => $offset];\n })\n ->sortDesc()\n ->keys()\n ->first();\n\n $now = Carbon::now($earliestTz);\n $isScheduledTime = (int) $now->format('H') === self::SENT_REPORT_AT_HOURS;\n\n if ($isScheduledTime) {\n return true;\n }\n\n return $this->hasPassedScheduledTime($generatedAt, $earliestTz);\n }\n\n public function hasPassedScheduledTime(?CarbonInterface $generatedAt, string $timezone): bool\n {\n if ($generatedAt === null) {\n return false;\n }\n\n $now = Carbon::now($timezone);\n $scheduledTime = $now->copy()->setTime(self::SENT_REPORT_AT_HOURS, 0, 0);\n\n if ($now->hour < self::SENT_REPORT_AT_HOURS) {\n $scheduledTime = $scheduledTime->subDay();\n }\n\n $scheduledTimeUtc = $scheduledTime->copy()->utc();\n $generatedAtUtc = $generatedAt->copy()->utc();\n $nowUtc = $now->copy()->utc();\n\n return $generatedAtUtc->lt($scheduledTimeUtc) && $nowUtc->gt($scheduledTimeUtc);\n }\n\n public function calculateFromAndToDatePeriod(\n string $frequency,\n ?Carbon $fromDate = null,\n ?Carbon $toDate = null,\n DateTimeZone|string|null $timezone = 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($timezone);\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()->subWeek()->startOfWeek(CarbonInterface::MONDAY),\n 'toDate' => $now->copy()->subWeek()->endOfWeek(CarbonInterface::SUNDAY),\n ],\n self::FREQUENCY_MONTHLY => [\n 'fromDate' => $now->copy()->subMonthNoOverflow()->startOfMonth(),\n 'toDate' => $now->copy()->subMonthNoOverflow()->endOfMonth(),\n ],\n self::FREQUENCY_QUARTERLY => [\n 'fromDate' => $now->copy()->subQuarterNoOverflow()->startOfQuarter(),\n 'toDate' => $now->copy()->subQuarterNoOverflow()->endOfQuarter(),\n ],\n default => throw new InvalidArgumentException(\"Unsupported frequency: {$frequency}\"),\n };\n }\n\n private function calculateFromAndToDate(AutomatedReport $automatedReport): array\n {\n return $this->calculateFromAndToDatePeriod(\n $automatedReport->getFrequency(),\n $automatedReport->getFrom(),\n $automatedReport->getTo()\n );\n }\n\n public function getAskJiminnyGenerateReportPayload(\n AutomatedReport $automatedReport,\n AutomatedReportResult $reportResult,\n array $activityIds,\n ): array {\n return [\n 'user_question' => $automatedReport->getAskAnythingPrompt()?->getContent(),\n 'call_ids' => array_map('strval', $activityIds),\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResult->getUuid(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->getReportPeriodName($reportResult),\n 'report_name' => $automatedReport->getCustomName(),\n ];\n }\n\n private function getCallbackUrl(): string\n {\n return $this->webhookService->route('jiminny.webhook.reports.ready');\n }\n\n /**\n * Validate and transform payload data for automated reports\n *\n * @param array $data\n *\n * @throws InvalidArgumentException\n *\n * @return array\n */\n private function validateAndTransformData(array $data): array\n {\n // Validate organization (team) and check feature\n $team = $this->validateOrganization($data['organization'] ?? null);\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $type = $this->validateReportType($data['report_type'] ?? null);\n $frequency = $this->validateFrequency($data['frequency'] ?? null);\n $additionalPromptInput = $this->validateAdditionalPromptInput(\n $data['additional_prompt_input'] ?? null\n );\n $customReportName = $this->validateCustomReportName($data['custom_name'] ?? null);\n\n // Prepare data for the database\n $reportData = [\n 'team_id' => $team->getId(),\n 'type' => $type,\n 'status' => $status,\n 'frequency' => $frequency,\n 'additional_prompt_input' => $additionalPromptInput,\n 'custom_name' => $customReportName,\n ];\n\n // Validate deal values\n $reportData = $this->validateDealValues($data, $reportData);\n\n // Validate date range\n $reportData = $this->validateDateRange($data, $reportData, $frequency);\n\n // Validate call durations\n $reportData = $this->validateCallDurations($data, $reportData);\n\n // Validate call types\n $reportData = $this->validateCallTypes($data, $reportData);\n\n // Validate media types\n $reportData = $this->validateMediaTypes($data, $reportData);\n\n // Validate groups\n if (isset($data['teams'])) {\n $reportData['groups'] = $this->validateAndGetGroupIds($team, $data['teams']);\n }\n\n // Validate deal stages\n $reportData = $this->validateDealStages($data, $reportData, $team, $type);\n\n // Validate playbook categories\n $reportData = $this->validatePlaybookCategories($data, $reportData, $team);\n\n // Validate recipients\n $reportData['recipients'] = [\n 'users' => $this->validateAndGetUserIdsByTeam($team, $data['recipients'] ?? []),\n ];\n\n if (isset($data['jiminny_recipients'])) {\n // Validate Jiminny recipients\n $reportData['jiminny_recipients'] = [\n 'users' => $this->validateAndGetJiminnyUserIds((array) $data['jiminny_recipients']),\n ];\n }\n\n return $reportData;\n }\n\n private function validateDealValues(array $data, array $reportData): array\n {\n if (isset($data['min_deal_value'])) {\n $reportData['deal_value_min'] = (int) $data['min_deal_value'];\n\n if ($reportData['deal_value_min'] > 4294967295 || $reportData['deal_value_min'] < 0) {\n throw new InvalidArgumentException('Min deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_deal_value'])) {\n $reportData['deal_value_max'] = (int) $data['max_deal_value'];\n\n if ($reportData['deal_value_max'] > 4294967295 || $reportData['deal_value_max'] < 0) {\n throw new InvalidArgumentException('Max deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['min_deal_value'], $data['max_deal_value'])\n && $data['min_deal_value'] > $data['max_deal_value']\n ) {\n throw new InvalidArgumentException('Min deal value cannot be greater than max deal value');\n }\n\n return $reportData;\n }\n\n private function validateDateRange(array $data, array $reportData, string $frequency): array\n {\n // Set date range only for one_off frequency\n if ($frequency === 'one_off') {\n if (isset($data['start_date_period'])) {\n $reportData['from'] = $this->parseDate($data['start_date_period']);\n }\n\n if (isset($data['end_date_period'])) {\n $reportData['to'] = $this->parseDate($data['end_date_period']);\n }\n\n if (empty($reportData['from']) || empty($reportData['to'])) {\n throw new InvalidArgumentException(\n 'Start date and end date are required for one_off frequency'\n );\n }\n } else {\n $reportData['from'] = null;\n $reportData['to'] = null;\n }\n\n return $reportData;\n }\n\n private function validateCallDurations(array $data, array $reportData): array\n {\n // Convert call durations from minutes to seconds\n if (isset($data['min_call_duration'])) {\n $reportData['call_duration_min'] = (int) $data['min_call_duration'] * 60;\n\n if ($reportData['call_duration_min'] > 4294967295 || $reportData['call_duration_min'] < 0) {\n throw new InvalidArgumentException('Min call duration should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_call_duration'])) {\n $reportData['call_duration_max'] = (int) $data['max_call_duration'] * 60;\n\n if ($reportData['call_duration_max'] > 4294967295 || $reportData['call_duration_max'] < 0) {\n throw new InvalidArgumentException('Max call duration should be between 0 and 4294967295');\n }\n }\n\n return $reportData;\n }\n\n private function validateCallTypes(array $data, array $reportData): array\n {\n // Set call types\n $reportData['call_types'] = $data['call_type'] ?? [];\n if (empty($reportData['call_types'])) {\n $reportData['call_types'] = self::getCallTypes();\n }\n\n foreach ($reportData['call_types'] as $callType) {\n if (! in_array($callType, self::getCallTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Call type %s is invalid', $callType));\n }\n }\n\n return $reportData;\n }\n\n private function validateMediaTypes(array $data, array $reportData): array\n {\n // Set media types from input data\n $reportData['media_types'] = $data['media_types'] ?? [];\n\n if (empty($reportData['media_types'])) {\n throw new InvalidArgumentException('Media types are required');\n }\n\n foreach ($reportData['media_types'] as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n throw new InvalidArgumentException(sprintf('Media type %s is invalid', $mediaType));\n }\n }\n\n return $reportData;\n }\n\n private function validateDealStages(array $data, array $reportData, Team $team, string $reportType): array\n {\n // Validate and set deal stages\n if (isset($data['deal_stage_at_call'])) {\n $reportData['deal_at_call_stages'] =\n $this->validateAndGetDealStageIds($team, $data['deal_stage_at_call'], 'Deal stage at call');\n }\n\n if (isset($data['current_deal_stage'])) {\n $reportData['current_deal_stages'] =\n $this->validateAndGetDealStageIds($team, $data['current_deal_stage'], 'Current deal stage');\n }\n\n // Ensure current_deal_stage is not provided for loss_analysis report type\n if ($reportType === self::TYPE_LOSS_ANALYSIS && ! empty($data['current_deal_stage'])) {\n throw new InvalidArgumentException('Current deal stage is not applicable for Loss Analysis reports');\n }\n\n return $reportData;\n }\n\n // transform uuid to id\n private function validatePlaybookCategories(array $data, array $reportData, Team $team): array\n {\n $key = 'playbook_categories';\n\n if (isset($data[$key])) {\n $payloadIds = $data[$key];\n $ids = [];\n\n foreach ($payloadIds as $uuid) {\n $uuid = (string) $uuid;\n\n try {\n $playbookCategory = $this->playbookCategoryRepository->findByUuid($uuid);\n } catch (Throwable $throwable) {\n Log::error(__METHOD__ . ' ' . $throwable->getMessage());\n\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory) {\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory->hasPlaybook()) {\n throw new InvalidArgumentException(sprintf('Playbook category %s has no playbook', $uuid));\n }\n\n if ($playbookCategory->getPlaybook()->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Playbook category %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $ids[] = $playbookCategory->getId();\n }\n\n $reportData[$key] = $ids;\n }\n\n return $reportData;\n }\n\n private function validateReportStatus($status): bool\n {\n if (! in_array($status, [true, false], true)) {\n throw new InvalidArgumentException('Report status is invalid');\n }\n\n return $status;\n }\n\n private function validateReportType($type): string\n {\n if (! in_array($type, self::getTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Report type is invalid: %s', $type));\n }\n\n return $type;\n }\n\n private function validateFrequency($frequency): string\n {\n if (! in_array($frequency, self::getFrequencies(), true)) {\n throw new InvalidArgumentException('Frequency is invalid');\n }\n\n return $frequency;\n }\n\n private function validateAdditionalPromptInput(?string $additionalPromptInput): ?string\n {\n if ($additionalPromptInput && strlen($additionalPromptInput) > 5000) {\n throw new InvalidArgumentException('Additional Prompt Input should be less than 5000 characters');\n }\n\n return $additionalPromptInput;\n }\n\n private function validateCustomReportName(?string $customReportName): ?string\n {\n if ($customReportName === null || $customReportName === '') {\n return null;\n }\n\n if (strlen($customReportName) > 70) {\n throw new InvalidArgumentException('Custom report name should be less than 70 characters');\n }\n\n return $customReportName;\n }\n\n private function validateOrganization(?string $organizationUuid): Team\n {\n if (! $organizationUuid) {\n throw new InvalidArgumentException('Organization is required');\n }\n\n $team = $this->teamRepository->idOrUuid($organizationUuid);\n\n if (! $team) {\n throw new InvalidArgumentException('Organization not found');\n }\n\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n throw new InvalidArgumentException('Organization does not have the Automated Reports feature');\n }\n\n return $team;\n }\n\n private function validateAndGetGroupIds(Team $team, array $teamUuids): array\n {\n $groupIds = [];\n\n foreach ($teamUuids as $uuid) {\n $group = $this->groupRepository->findByUuid($uuid);\n\n if ($group === null || $group->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Group %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $groupIds[] = $group->getId();\n\n }\n\n return $groupIds;\n }\n\n private function validateAndGetDealStageIds(Team $team, array $stageUuids, string $propertyLabel): array\n {\n $stageIds = [];\n\n foreach ($stageUuids as $uuid) {\n $stage = $this->stageRepository->findByUuid($uuid);\n\n if ($stage === null || $stage->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Stage %s not found for team %s for %s', $uuid, $team->getUuid(), $propertyLabel)\n );\n }\n\n $stageIds[] = $stage->getId();\n }\n\n return $stageIds;\n }\n\n private function validateAndGetUserIds(array $userUuids, callable $teamCheck): array\n {\n if (empty($userUuids)) {\n return [];\n }\n\n $userIds = [];\n\n foreach ($userUuids as $uuid) {\n $user = $this->userRepository->findByUuid($uuid);\n\n if (! $user || ! $user->isStatusActive()) {\n throw new InvalidArgumentException(\n sprintf('User %s not found or is not active', $uuid)\n );\n }\n\n if (! $teamCheck($user)) {\n throw new InvalidArgumentException(\n sprintf('User %s does not belong to the allowed team(s)', $uuid)\n );\n }\n\n $userIds[] = $user->getId();\n }\n\n return $userIds;\n }\n\n private function validateAndGetUserIdsByTeam(Team $team, array $userUuids): array\n {\n return $this->validateAndGetUserIds($userUuids, fn ($user) => $user->getTeamId() === $team->getId());\n }\n\n private function validateAndGetJiminnyUserIds(array $userUuids): array\n {\n $allowedTeamIds = config('kiosk.teamIds', []);\n\n return $this->validateAndGetUserIds($userUuids, fn ($user) => in_array($user->getTeamId(), $allowedTeamIds, true));\n }\n\n private function parseDate(string $dateString): string\n {\n return date('Y-m-d H:i:s', strtotime($dateString));\n }\n\n private function generateReportResultViewUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.view', ['uuid' => $result->getUuid()]);\n }\n\n private function generateReportResultDownloadUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.download', ['uuid' => $result->getUuid()]);\n }\n\n private function getReportMediaRouteResource(AutomatedReportResult $result): string\n {\n if ($result->getMediaType() === self::MEDIA_TYPE_PDF) {\n return self::PDF_KEY;\n } elseif ($result->getMediaType() === self::MEDIA_TYPE_PODCAST) {\n return self::AUDIO_KEY;\n }\n\n throw new \\InvalidArgumentException('Unknown media type.');\n }\n\n public function getMediaPath(AutomatedReportResult $result): ?string\n {\n $url = match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => $result->getPdfUrl(),\n self::MEDIA_TYPE_PODCAST => $result->getPodcastAudioUrl(),\n default => null,\n };\n\n if ($url === null) {\n return null;\n }\n\n $path = parse_url(trim($url, '\"\\''), PHP_URL_PATH);\n\n return $path ?: null;\n }\n\n public function getFilenameSuffix(AutomatedReportResult $result): ?string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => 'Podcast',\n default => null,\n };\n }\n\n public function getMailSubjectSuffix(AutomatedReportResult $result): string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => 'report',\n self::MEDIA_TYPE_PODCAST => 'podcast',\n default => '',\n };\n }\n\n public function getMediaTypeMetadata(AutomatedReportResult $result): array\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => ['extension' => 'mp3', 'mime' => 'audio/mpeg'],\n self::MEDIA_TYPE_PDF => ['extension' => 'pdf', 'mime' => 'application/pdf'],\n default => ['extension' => null, 'mime' => null],\n };\n }\n\n public function deleteS3Files(AutomatedReportResult $result): void\n {\n $teamUuid = $result->getReport()->getTeam()->getUuid();\n $reportUuid = $result->getUuid();\n\n // delete all files for a report uuid no mather of pdf, podcast, or both\n // in case of both - the podcast files are linked to the pdf (parent) uuid\n // pdf and podcast date times should be close\n $path = sprintf('%s/%s/%s', $teamUuid, self::S3_DIR, $reportUuid);\n\n foreach (self::FILE_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted S3 file', [\n 'path' => $file,\n ]);\n }\n }\n\n foreach (self::FILE_PODCAST_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '_podcast.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted Podcast S3 file', [\n 'path' => $file,\n ]);\n }\n }\n }\n\n /**\n *\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): Collection\n {\n return $this->automatedReportsRepository->getTeamIdsWithReportsResults($teamId);\n }\n\n /**\n * Core delete logic for report results using a query\n *\n * @param Builder $query\n * @param array $logContext\n *\n * @return int\n */\n private function deleteReportResultsByQuery(Builder $query, array $logContext = []): int\n {\n $deletedCount = 0;\n\n if ($query->exists()) {\n Log::info(\n 'Run delete report results',\n array_merge(\n $logContext,\n [\n 'service' => 'AutomatedReportsService',\n ]\n )\n );\n\n $query->chunkById(50, function ($results) use (&$deletedCount, $logContext) {\n foreach ($results as $result) {\n $this->deleteReportResult($result);\n $deletedCount++;\n\n Log::info(\n 'Deleted a report result',\n array_merge(\n $logContext,\n [\n 'result_id' => $result->getId(),\n 'report_id' => $result->getReportId(),\n ]\n )\n );\n }\n });\n }\n\n return $deletedCount;\n }\n\n /**\n * Delete report results for a team by retention period\n *\n * @param Team $team\n * @param CarbonImmutable $retentionDate\n *\n * @return int Number of deleted report results\n */\n public function deleteReportsResultsInRetentionPeriod(Team $team, CarbonImmutable $retentionDate): int\n {\n $reportIds = $this->automatedReportsRepository->getReportIdsByTeam($team);\n\n if ($reportIds->isEmpty()) {\n return 0;\n }\n\n $query = $this->automatedReportsRepository\n ->getReportResultsQueryForRetention($team, $retentionDate);\n\n return $this->deleteReportResultsByQuery($query, [\n 'team_id' => $team->getId(),\n 'retention_date' => $retentionDate->toDateTimeString(),\n ]);\n }\n\n /**\n * Delete ALL report results for a specific automated report\n *\n * @param string $uuid\n *\n * @return int\n */\n public function deleteReportResults(string $uuid): int\n {\n $report = $this->getReport($uuid);\n\n $query = $this->automatedReportsRepository->getResultsByReportQuery($report);\n\n return $this->deleteReportResultsByQuery($query, [\n 'report_uuid' => $uuid,\n 'report_id' => $report->getId(),\n ]);\n }\n\n public function deleteReportResult(AutomatedReportResult $result): void\n {\n $this->deleteS3Files($result);\n\n $result->delete();\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getTeamReports(Team $team): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getReportsByTeam($team);\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getReportResults(AutomatedReport $report): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getResultsByReport($report);\n }\n\n public function deleteAllReportResults(AutomatedReport $report): void\n {\n $results = $this->getReportResults($report);\n\n /** @var AutomatedReportResult $result */\n foreach ($results as $result) {\n Log::info('Deleting result', [\n 'report' => $report->getId(),\n 'result' => $result->getId(),\n ]);\n\n $this->deleteReportResult($result);\n }\n }\n\n public function deleteAllData(Team $team): void\n {\n Log::info('Deleting automated report and results for team', [\n 'team' => $team->getId(),\n ]);\n\n $reports = $this->getTeamReports($team);\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n Log::info('Deleting report', [\n 'team' => $team->getId(),\n 'report' => $report->getId(),\n ]);\n\n $this->deleteAllReportResults($report);\n\n $report->delete();\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Carbon\\CarbonImmutable;\nuse Carbon\\CarbonInterface;\nuse Carbon\\Exceptions\\InvalidFormatException;\nuse DateTime;\nuse DateTimeInterface;\nuse DateTimeZone;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\InputTypeEnum;\nuse Jiminny\\Component\\AskAnything\\AskAnythingPromptService;\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Contracts\\Repositories\\PlaybookCategoryRepository;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\ModelNotFoundException;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Partner;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Repositories\\GroupRepository;\nuse Jiminny\\Repositories\\SearchRepository;\nuse Jiminny\\Repositories\\StageRepository;\nuse Throwable;\n\nclass AutomatedReportsService\n{\n public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';\n public const string TYPE_ASK_JIMINNY = 'ask_jiminny';\n\n /**\n * Standard report types (used by kiosk for existing automated reports).\n */\n // @TODO this will add filter, however if we need to control feature by FF we need conditional logic\n public const array TYPES = [\n ['id' => 'exec_summary', 'name' => 'Exec Summary'],\n ['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],\n ['id' => 'product_feedback', 'name' => 'Product Feedback'],\n ['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],\n// ['id' => 'questions', 'name' => 'Questions'],\n// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],\n ];\n\n public const array ALL_TYPES = [\n ...self::TYPES,\n ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],\n ];\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\n /**\n * Frequencies for standard (non-Ask Jiminny) reports.\n */\n public const array FREQUENCIES = [\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n\n /**\n * Frequencies for Ask Jiminny reports.\n */\n public const array ASK_JIMINNY_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ];\n\n public const string MEDIA_TYPE_PDF = 'pdf';\n public const string MEDIA_TYPE_PODCAST = 'podcast';\n public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];\n public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];\n public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];\n public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];\n\n public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];\n public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];\n public const int SENT_REPORT_AT_HOURS = 5;\n public const string PDF_KEY = 'pdf';\n public const string AUDIO_KEY = 'audio';\n\n private const array ALL_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n private const string S3_DIR = 'reports';\n private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];\n private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];\n\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly GroupRepository $groupRepository,\n private readonly UserRepository $userRepository,\n private readonly StageRepository $stageRepository,\n private readonly DealStagesService $dealStagesService,\n private readonly RecipientsService $recipientsService,\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly Webhook $webhookService,\n private readonly BusDispatcher $dispatcher,\n private readonly ActivityTypeService $activityTypeService,\n private readonly PlaybookCategoryRepository $playbookCategoryRepository,\n private readonly AskAnythingPromptService $askAnythingPromptService,\n private readonly SearchRepository $activitySearchRepository,\n private readonly AskAnythingRepository $askAnythingRepository,\n ) {\n }\n\n public static function getTypes(): array\n {\n $types = self::TYPES;\n\n return array_map(static function ($type) {\n return $type['id'];\n }, $types);\n }\n\n public static function getCallTypes(): array\n {\n return array_map(static function ($callType) {\n return $callType['id'];\n }, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);\n }\n\n public static function getFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::FREQUENCIES);\n }\n\n // front-facing structure\n public function getReportEnabledFieldData(bool $value = false): array\n {\n return [\n 'id' => 'report_enabled',\n 'label' => '',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'value' => $value,\n ];\n }\n\n // Organizations = Teams\n public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array\n {\n $options = $this->getTeams(partner: $partner);\n\n if ($shortVersion) {\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value,\n 'dependencies' => [\n 'teams',\n 'deal_stage_at_call',\n 'current_deal_stage',\n 'recipients',\n ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,\n ],\n 'dependsOn' => [],\n ];\n }\n\n // Teams = Groups\n public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array\n {\n if ($shortVersion) {\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value, // value should be an array of objects {id, name}\n 'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],\n 'dependsOn' => [],\n ];\n }\n\n public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array\n {\n $types = [];\n if ($team instanceof Team) {\n if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n $types = self::TYPES;\n }\n if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {\n $types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];\n }\n } else {\n $types = self::TYPES;\n }\n\n if ($shortVersion) {\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'options' => $types,\n ];\n }\n\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $types,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getFrequencyFieldData(?string $value = null): array\n {\n return [\n 'id' => 'frequency',\n 'label' => 'Frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::FREQUENCIES,\n 'value' => $value,\n 'dependencies' => ['period'],\n 'dependsOn' => [],\n ];\n }\n\n public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array\n {\n return [\n 'id' => 'period',\n 'label' => 'Select one-off period',\n 'inputType' => InputTypeEnum::DATE_RANGE,\n 'required' => true,\n 'placeholder' => 'Select',\n 'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],\n 'queryParams' => [\n 'startDate' => 'start_date_period',\n 'endDate' => 'end_date_period',\n ],\n 'dependencies' => [],\n 'dependsOn' => ['frequency'],\n ];\n }\n\n public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array\n {\n return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);\n }\n\n public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);\n }\n\n public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);\n }\n\n public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'deal_value',\n 'label' => 'Deal Value',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_deal_value',\n 'max' => 'max_deal_value',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array\n {\n $value = [];\n $conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;\n $dialerOn && $value[] = self::CALL_TYPE_DIALER;\n\n return [\n 'id' => 'call_type',\n 'label' => 'Call Type',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => [\n self::CALL_TYPE_CONFERENCE,\n self::CALL_TYPE_DIALER,\n ],\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getMediaTypeFieldData(?AutomatedReport $report = null): array\n {\n $value = [];\n\n if ($report) {\n $value = $this->transformMediaTypes($report);\n }\n\n return [\n 'id' => 'media_types',\n 'label' => 'Export as',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => self::MEDIA_TYPE_OBJECTS,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'call_duration',\n 'label' => 'Call Duration',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_call_duration',\n 'max' => 'max_call_duration',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getRecipientsFieldData(?Team $team = null, array $value = []): array\n {\n return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);\n }\n\n public function getJiminnyRecipientsFieldData(array $value = []): array\n {\n return $this->recipientsService->getJiminnyRecipientsFieldData($value);\n }\n\n public function getAdditionalPromptInputFieldData(?string $value = null): array\n {\n return [\n 'id' => 'additional_prompt_input',\n 'label' => 'Special requirements',\n 'inputType' => InputTypeEnum::TEXTAREA,\n 'required' => false,\n 'placeholder' => 'What should be the focus of the report?',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCustomReportNameFieldData(?string $value = null): array\n {\n return [\n 'id' => 'custom_name',\n 'label' => 'Custom report name',\n 'inputType' => InputTypeEnum::TEXT,\n 'required' => false,\n 'placeholder' => 'Enter custom name',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n // data providers\n public function getTeams(?Partner $partner = null): array\n {\n $teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);\n\n $teamData = [];\n foreach ($teams as $team) {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n continue;\n }\n\n $teamData[] = $this->transformTeam($team);\n }\n\n return $teamData;\n }\n\n public function getTeamGroups(string $teamUuid): array\n {\n $data = [];\n $team = $this->getTeam($teamUuid);\n\n if ($team !== null) {\n $groups = $team->groups()->get();\n\n foreach ($groups as $group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array\n {\n $data = [];\n $teams = $this->getTeams(partner: $partner);\n\n foreach ($teams as $team) {\n if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {\n continue;\n }\n\n $data[] = [\n 'label' => $team['name'],\n 'groups' => $this->getTeamGroups($team['id']),\n ];\n }\n\n return $data;\n }\n\n public function getTeam(string $teamUuid): ?Team\n {\n return $this->teamRepository->idOrUuid($teamUuid);\n }\n\n public function getTeamById(int $teamId): ?Team\n {\n return $this->teamRepository->find($teamId);\n }\n\n public function getGroupsUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportGroups = $report->getGroups();\n foreach ($reportGroups as $groupId) {\n if ($group = $this->groupRepository->find($groupId)) {\n $uuids[] = $group->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getPlaybookCategoriesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $playbookCategories = $report->getPlaybookCategories();\n foreach ($playbookCategories as $id) {\n if ($category = $this->playbookCategoryRepository->find($id)) {\n $uuids[] = $category->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getDealAtCallStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getDealAtCallStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getCurrentDealStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getCurrentDealStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getRecipients());\n }\n\n public function getJiminnyUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function extractUserUuids(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => $user->getUuid())\n ->values()\n ->all();\n }\n\n // get mail data\n public function getRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getRecipients());\n }\n\n /**\n * @return array<UserContract>\n */\n public function getRecipientUserObjects(AutomatedReport $report): array\n {\n $userIds = $report->getRecipients()['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->values()\n ->all();\n }\n\n private function getJiminnyRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function buildRecipientUsers(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => [\n 'email' => $user->getEmailAddress(),\n 'name' => $user->getName(),\n 'timezone' => $user->getTimezone()->getName(),\n ])\n ->values()\n ->all();\n }\n\n public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array\n {\n if ($report->isAskJiminnyReport()) {\n $recipients = $this->resolveAskJiminnyRecipients($report);\n } else {\n $recipients = $this->getRecipientUsers($report);\n if ($includeJiminny) {\n $recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));\n }\n }\n\n $emails = [];\n\n return array_values(array_filter(\n $recipients,\n static function ($recipient) use (&$emails) {\n if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {\n return false;\n }\n $emails[] = $recipient['email'];\n\n return true;\n }\n ));\n }\n\n private function resolveAskJiminnyRecipients(AutomatedReport $report): array\n {\n $recipients = [];\n\n $creator = $report->getCreator();\n if ($creator !== null) {\n $recipients[] = [\n 'email' => $creator->getEmailAddress(),\n 'name' => $creator->getName(),\n 'timezone' => $creator->getTimezone()->getName(),\n ];\n }\n\n return array_merge(\n $recipients,\n $this->buildRecipientUsers($report->getRecipients()),\n $this->getGroupRecipientUsers($report),\n );\n }\n\n private function getGroupRecipientUsers(AutomatedReport $report): array\n {\n $users = [];\n foreach ($report->getGroups() as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group === null) {\n continue;\n }\n foreach ($group->getMembers() as $member) {\n $users[] = [\n 'email' => $member->getEmailAddress(),\n 'name' => $member->getName(),\n 'timezone' => $member->getTimezone()->getName(),\n ];\n }\n }\n\n return $users;\n }\n\n public function getReportTypeName(AutomatedReportResult $report): string\n {\n $type = $report->getReport()->getType();\n\n $getType = $this->transformReportType($type);\n\n return $getType['name'];\n }\n\n public function getReportPeriodName(AutomatedReportResult $report): string\n {\n $from = $report->getFromDate();\n $to = $report->getToDate();\n $frequency = $report->getReport()->getFrequency();\n\n if ($from === null || $to === null) {\n if (! $report->getReport()->isAskJiminnyReport()) {\n $invalidPeriod = $from === null ? 'from' : 'to';\n\n throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);\n }\n\n $timezone = $report->getReport()->getCreator()?->getTimezone();\n $period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);\n $from = $period['fromDate'];\n $to = $period['toDate'];\n }\n\n return $this->formatReportPeriodName($frequency, $from, $to);\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 getReportTeamsName(AutomatedReportResult $report): string\n {\n $groups = $report->getGroups();\n\n if (empty($groups)) {\n return 'All';\n }\n\n // Get group names from repository\n $groupNames = [];\n foreach ($groups as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group) {\n $groupNames[] = $group->getName();\n }\n }\n\n if (count($groupNames) === 1) {\n // Single team format\n $teamsName = $groupNames[0];\n } else {\n // Multiple teams format\n $teamsName = implode(', ', $groupNames);\n }\n\n return $teamsName;\n }\n\n public function getReportFileName(AutomatedReportResult $report): string\n {\n $customName = $report->getReport()->getCustomName();\n $periodName = $this->getReportPeriodName($report);\n $filenameSuffix = $this->getFilenameSuffix($report);\n\n if ($customName) {\n if ($filenameSuffix) {\n $customName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$customName} - {$periodName}\");\n }\n\n $baseName = $this->getReportTypeName($report);\n\n if ($filenameSuffix) {\n $baseName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}\");\n }\n\n public function getReportFileNameWithExtension(AutomatedReportResult $result): string\n {\n $extension = $this->getMediaTypeMetadata($result)['extension'];\n\n return $this->getReportFileName($result) . '.' . $extension;\n }\n\n public function sanitizeFileName(string $fileName): string\n {\n return str_replace(['/', '\\\\'], '-', $fileName);\n }\n\n public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool\n {\n $recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);\n if (in_array($user->getId(), $recipientIds, true)) {\n return true;\n }\n\n if ($report->isAskJiminnyReport()) {\n $groupId = $user->getGroupId();\n if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {\n return true;\n }\n }\n\n return false;\n }\n\n public function transformReportResults(Collection $automatedReportResults): array\n {\n $data = [];\n foreach ($automatedReportResults as $automatedReportResult) {\n /** @var AutomatedReportResult $automatedReportResult */\n\n $report = $automatedReportResult->getReport();\n\n $createdBy = $report->getCreator();\n $creator = [\n 'id' => $createdBy?->getUuid(),\n 'name' => $createdBy?->getName(),\n 'email' => $createdBy?->getEmailAddress(),\n 'photoUrl' => $createdBy?->getPhotoUrl(),\n ];\n\n $data[] = [\n 'id' => $automatedReportResult->getUuid(),\n 'name' => $automatedReportResult->getName(),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n 'recipients' => $this->buildRecipients($report),\n 'report_type' => $this->transformReportType($report->getType()),\n 'media_type' => $automatedReportResult->getMediaType(),\n 'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),\n 'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),\n 'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),\n 'creator' => $creator,\n ];\n }\n\n return $data;\n }\n\n private function buildRecipients(AutomatedReport $report): array\n {\n $creatorUuid = $report->getCreator()?->getUuid();\n\n $recipients = array_values(array_filter(\n $this->transformRecipients($report->getRecipients()),\n static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,\n ));\n\n if (! $report->isAskJiminnyReport()) {\n return $recipients;\n }\n\n return [\n ...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),\n ...$recipients,\n ];\n }\n\n public function hasCallTypeConference(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);\n }\n\n public function hasCallTypeDialer(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);\n }\n\n // transformers\n private function transformTeam(Team $team): array\n {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n return [];\n }\n\n return [\n 'id' => $team->getUuid(),\n 'name' => $team->getName(),\n ];\n }\n\n private function transformReportFullView(AutomatedReport $report): array\n {\n $base = $this->transformReportBase($report);\n\n return $report->getType() === self::TYPE_ASK_JIMINNY\n ? $base + $this->transformAskJiminnyFields($report)\n : $base + $this->transformStandardReportFields($report);\n }\n\n private function transformReportBase(AutomatedReport $report): array\n {\n return [\n 'id' => $report->getUuid(),\n 'organization' => $this->transformOrganization(team: $report->getTeam()),\n 'report_type' => $this->transformReportType($report->getType()),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n ];\n }\n\n private function transformStandardReportFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n\n return [\n 'report_enabled' => $report->getStatus(),\n 'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),\n 'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),\n 'deal_value_min' => $report->getDealValueMin(),\n 'deal_value_max' => $report->getDealValueMax(),\n 'call_types' => $this->transformCallType($report->getCallTypes()),\n 'media_types' => $this->transformMediaTypes($report),\n 'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),\n 'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),\n 'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),\n 'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),\n 'recipients' => $this->transformRecipients($report->getRecipients()),\n 'created_by' => $this->transformCreator($report->getCreator()),\n 'additional_prompt_input' => $report->getAdditionalPromptInput(),\n 'custom_name' => $report->getCustomName(),\n 'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),\n 'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),\n 'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),\n ];\n }\n\n private function transformAskJiminnyFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n $creatorId = $report->getAttribute('created_by');\n $explicitUserIds = array_values(array_filter(\n $report->getRecipients()['users'] ?? [],\n static fn ($id) => $id !== $creatorId\n ));\n\n return [\n 'report_name' => $report->getCustomName(),\n 'enabled' => $report->getStatus(),\n 'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),\n 'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),\n 'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),\n 'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),\n ];\n }\n\n private function transformOrganization(?Team $team): array\n {\n return [\n 'id' => $team?->getUuid(),\n 'name' => $team?->getName(),\n ];\n }\n\n private function transformReportType(string $type): array\n {\n foreach (self::ALL_TYPES as $typeItem) {\n if ($typeItem['id'] === $type) {\n return $typeItem;\n }\n }\n\n return [];\n }\n\n private function transformCallType(array $types): array\n {\n $result = [];\n $callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];\n\n foreach ($types as $type) {\n foreach ($callTypes as $callTypeItem) {\n if ($callTypeItem['id'] === $type) {\n $result[] = $callTypeItem;\n\n break;\n }\n }\n }\n\n return $result;\n }\n\n private function transformMediaTypes(AutomatedReport $report): array\n {\n $values = [];\n\n foreach ($report->getMediaTypes() as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n continue;\n }\n\n $values[] = match ($mediaType) {\n self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,\n self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,\n };\n }\n\n return $values;\n }\n\n private function transformFrequency(string $frequency): array\n {\n foreach (self::ALL_FREQUENCIES as $frequencyItem) {\n if ($frequencyItem['id'] === $frequency) {\n return $frequencyItem;\n }\n }\n\n return [];\n }\n\n public function transformDurationToMinutes(?int $duration): ?int\n {\n if (! $duration) {\n return null;\n }\n\n return (int) ($duration / 60);\n }\n\n private function transformGroups(?Team $team, array $groupsIds): array\n {\n if (empty($groupsIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($groupsIds as $groupId) {\n $group = $team->groups()->where('id', $groupId)->first();\n\n if ($group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n 'photoUrl' => $group->getPhotoUrl(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformStages(?Team $team, array $stagesIds): array\n {\n if (empty($stagesIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($stagesIds as $stageId) {\n $stage = $team->stages()->where('id', $stageId)->first();\n\n if ($stage) {\n $data[] = [\n 'id' => $stage->getUuid(),\n 'name' => $stage->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformRecipients(array $recipients): array\n {\n $users = [];\n foreach ($recipients['users'] ?? [] as $userId) {\n $users[] = $this->transformUser($userId);\n }\n\n return $users;\n }\n\n private function transformCreator(?User $user): ?array\n {\n if ($user === null) {\n return null;\n }\n\n return $this->transformUser($user->getId());\n }\n\n private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array\n {\n if ($prompt === null) {\n return null;\n }\n\n return [\n 'id' => $prompt->getUuid(),\n 'name' => $prompt->getTitle(),\n ];\n }\n\n private function transformSafeSearch(?Search $search): ?array\n {\n if ($search === null) {\n return null;\n }\n\n return [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ];\n }\n\n private function transformUser(int $userId): array\n {\n /* @var ?User $user */\n $user = $this->userRepository->find($userId);\n\n return [\n 'id' => $user?->getUuid(),\n 'name' => $user?->getName(),\n 'email' => $user?->getEmailAddress(),\n 'photoUrl' => $user?->getPhotoUrl(),\n ];\n }\n\n public function create(array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $validatedData['created_by'] = auth()->id();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function update(string $uuid, array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $report = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $report) {\n throw new InvalidArgumentException('Report not found');\n }\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Create an Ask Jiminny report.\n */\n public function createAskJiminnyReport(array $data, User $creator): array\n {\n $validatedData = $this->validateAskJiminnyReportData($data, $creator);\n $validatedData['created_by'] = $creator->getId();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Update an Ask Jiminny report.\n */\n public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array\n {\n if (! $report->isAskJiminnyReport()) {\n throw new InvalidArgumentException('Report is not an Ask Jiminny report');\n }\n\n $validatedData = $this->validateAskJiminnyReportData($data, $user);\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array\n {\n if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {\n throw new InvalidArgumentException(\n 'This report is missing a saved search or prompt. ' .\n 'Edit the report to complete the setup before enabling it.'\n );\n }\n\n $this->automatedReportsRepository->update($report, ['status' => $status]);\n\n return $this->transformReportFullView($report->fresh());\n }\n\n /**\n * Validate and transform data for Ask Jiminny reports.\n */\n private function validateAskJiminnyReportData(array $data, User $user): array\n {\n // Validate name\n $name = trim($data['report_name'] ?? '');\n if (empty($name)) {\n throw new InvalidArgumentException('Report name is required');\n }\n if (mb_strlen($name) > 50) {\n throw new InvalidArgumentException('Report name must be 50 characters or less');\n }\n\n // Validate frequency (only daily, weekly, monthly for Ask Jiminny)\n $frequency = $data['frequency'] ?? null;\n $askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];\n if (! in_array($frequency, $askJiminnyFrequencies, true)) {\n throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');\n }\n\n // Validate expiration date\n $expiresAt = $data['expires_on'] ?? null;\n if (empty($expiresAt)) {\n throw new InvalidArgumentException('Expiration date is required');\n }\n\n try {\n $expiresAtDate = Carbon::parse($expiresAt);\n } catch (InvalidFormatException $e) {\n throw new InvalidArgumentException('Expiration date format is invalid');\n }\n $maxExpiration = Carbon::now()->addYear()->endOfDay();\n if ($expiresAtDate->gt($maxExpiration)) {\n throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');\n }\n if ($expiresAtDate->isPast()) {\n throw new InvalidArgumentException('Expiration date cannot be in the past');\n }\n\n // Validate saved search\n $activitySearchId = $data['saved_search'] ?? null;\n if (empty($activitySearchId)) {\n throw new InvalidArgumentException('Saved search is required');\n }\n $savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);\n if (! $savedSearch) {\n throw new InvalidArgumentException('Saved search not found or does not belong to you');\n }\n\n // Validate saved prompt\n $askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;\n if (empty($askAnythingPromptId)) {\n throw new InvalidArgumentException('Ask Jiminny prompt is required');\n }\n $prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);\n if (! $prompt) {\n throw new InvalidArgumentException('Ask Jiminny prompt not found');\n }\n\n // Validate status\n $status = $data['enabled'] ?? false;\n\n $recipientUserIds = [$user->getId()];\n\n if (! empty($data['share_users'])) {\n $sharedUserIds = $this->validateAndGetUserIdsByTeam(\n $user->team,\n (array) $data['share_users']\n );\n $recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);\n }\n\n $sharedGroupIds = [];\n if (! empty($data['share_teams'])) {\n $sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);\n }\n\n $recipientUserIds = array_values(array_unique($recipientUserIds));\n\n return [\n 'team_id' => $user->getTeamId(),\n 'type' => self::TYPE_ASK_JIMINNY,\n 'status' => (bool) $status,\n 'frequency' => $frequency,\n 'custom_name' => $name,\n 'activity_search_id' => $savedSearch->getId(),\n 'ask_anything_prompt_id' => $prompt->getId(),\n 'expires_at' => $expiresAtDate->toDateString(),\n 'media_types' => [self::MEDIA_TYPE_PDF],\n 'call_types' => [],\n 'recipients' => ['users' => $recipientUserIds],\n 'groups' => $sharedGroupIds,\n ];\n }\n\n public static function getAskJiminnyFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::ASK_JIMINNY_FREQUENCIES);\n }\n\n public function getAskJiminnyReportFilters(User $user): array\n {\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n return [\n [\n 'id' => 'prompt',\n 'label' => 'Prompt',\n 'options' => $prompts,\n ],\n [\n 'id' => 'saved_search',\n 'label' => 'Saved Search',\n 'options' => $savedSearches,\n ],\n ];\n }\n\n public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array\n {\n $team = $user->getTeam();\n $userTimezone = $user->getTimezone();\n\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n $teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ])->values()->all();\n\n $shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];\n\n $sharedTeamsValue = [];\n $sharedUsersValue = [];\n if ($report) {\n $sharedTeamsValue = $this->transformGroups($team, $report->getGroups());\n\n $recipientUserIds = $report->getRecipients()['users'] ?? [];\n $creatorId = $report->getAttribute('created_by');\n $sharedUserIds = array_values(array_filter(\n $recipientUserIds,\n static fn ($id) => $id !== $creatorId\n ));\n $sharedUsersValue = collect($sharedUserIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (User $u) => [\n 'id' => $u->getUuid(),\n 'name' => $u->getName(),\n ])\n ->values()\n ->all();\n }\n\n return [\n 'fields' => [\n [\n 'id' => 'enabled',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'label' => '',\n 'value' => $report?->getStatus() ?? false,\n ],\n [\n 'id' => 'report_name',\n 'inputType' => InputTypeEnum::TEXT,\n 'label' => 'Name',\n 'placeholder' => 'Enter name',\n 'required' => true,\n 'validation' => ['maxLength' => 50],\n 'value' => $report?->getCustomName() ?? '',\n ],\n [\n 'id' => 'frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Frequency',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::ASK_JIMINNY_FREQUENCIES,\n 'value' => $report ? $this->transformFrequency($report->getFrequency()) : null,\n ],\n [\n 'id' => 'expires_on',\n 'inputType' => InputTypeEnum::DATE,\n 'label' => 'Expires on',\n 'required' => true,\n 'placeholder' => 'Select',\n 'validation' => [\n 'minDate' => now($userTimezone)->toDateString(),\n 'maxDate' => now($userTimezone)->addYear()->toDateString(),\n ],\n 'value' => $report?->getExpiresAt()?->toDateString(),\n ],\n [\n 'id' => 'share_teams',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team',\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $teamGroups,\n 'value' => $sharedTeamsValue,\n ],\n [\n 'id' => 'share_users',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team member',\n 'required' => false,\n 'placeholder' => 'Select',\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'options' => $shareUsers,\n 'value' => $sharedUsersValue,\n ],\n [\n 'id' => 'saved_search',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Saved search',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $savedSearches,\n 'value' => $report && $report->getSavedSearch() ? [\n 'id' => $report->getSavedSearch()->getUuid(),\n 'name' => $report->getSavedSearch()->getName(),\n ] : null,\n ],\n [\n 'id' => 'ask_jiminny_prompt',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Ask Jiminny prompt',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $prompts,\n 'value' => $report && $report->getAskAnythingPrompt() ? [\n 'id' => $report->getAskAnythingPrompt()->getUuid(),\n 'name' => $report->getAskAnythingPrompt()->getTitle(),\n ] : null,\n ],\n ],\n ];\n }\n\n private function updateResultNames(AutomatedReport $automatedReport): void\n {\n $results = $this->automatedReportsRepository->getResultsByReport($automatedReport);\n\n foreach ($results as $result) {\n $result->update(['name' => $this->getReportFileName($result)]);\n }\n }\n\n public function updateStatus(string $uuid, array $data): array\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $automatedReport->update([\n 'status' => $status,\n ]);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n private function generateOneOffReport(AutomatedReport $automatedReport): void\n {\n // the scheduler handles all the other frequency types\n if ($automatedReport->getStatus() === false || $automatedReport->getFrequency() !== self::FREQUENCY_ONE_OFF) {\n return;\n }\n\n $this->dispatcher->dispatch(new RequestGenerateReportJob($automatedReport->getUuid()));\n }\n\n public function getReport(string $uuid, ?Partner $partner = null): AutomatedReport\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n if ($partner !== null && ! $partner->isDefaultPartner() && $automatedReport->team->partner_id !== $partner->getId()) {\n throw new ModelNotFoundException('Report not found');\n }\n\n return $automatedReport;\n }\n\n public function get(string $uuid, ?Partner $partner = null): array\n {\n $automatedReport = $this->getReport($uuid, $partner);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function list(string $sortColumn = 'created_at', string $sortDirection = 'desc', ?Partner $partner = null): array\n {\n $results = [];\n $collection = $this->automatedReportsRepository->getAllStandardReports($sortColumn, $sortDirection, $partner);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function listAskJiminnyReports(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): array {\n $results = [];\n $collection = $this->automatedReportsRepository->getAskJiminnyReportsByUser($user, $sortColumn, $sortDirection);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function delete(string $uuid): void\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $automatedReport->delete();\n }\n\n public function createReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n return $this->automatedReportsRepository->createResult(\n array_merge(\n [\n 'report_id' => $automatedReport->getId(),\n 'status' => AutomatedReportResult::STATUS_DEFAULT,\n ],\n $data\n )\n );\n }\n\n public function getOrCreateReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n $existing = $this->automatedReportsRepository->findLatestSameDayDefaultOrFailedResult($automatedReport);\n\n if ($existing !== null) {\n $existing->update(['status' => AutomatedReportResult::STATUS_DEFAULT]);\n\n return $existing;\n }\n\n return $this->createReportResult($automatedReport, $data);\n }\n\n public function getReportResult(string $resultUuid): AutomatedReportResult\n {\n $report = $this->automatedReportsRepository->findResultByUuid($resultUuid);\n\n if (! $report) {\n throw new ModelNotFoundException('Report Result not found');\n }\n\n return $report;\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return $this->automatedReportsRepository->findChildResult($result, $type);\n }\n\n // prophet API calls\n /**\n * @throws ApplicationException\n */\n public function getGenerateReportPayload(AutomatedReport $automatedReport, string $reportResultUuid): array\n {\n $period = $this->calculateFromAndToDate($automatedReport);\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n return [\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResultUuid,\n 'report_type' => $automatedReport->getType(),\n 'media_types' => $automatedReport->getMediaTypes(),\n 'from_date' => $fromDate->startOfDay()->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->endOfDay()->format(DateTimeInterface::RFC3339),\n 'group_ids' => $automatedReport->getGroups(),\n 'call_deal_stage' => $automatedReport->getDealAtCallStages(),\n 'current_deal_stage' => $automatedReport->getCurrentDealStages(),\n 'deal_min_value' => $automatedReport->getDealValueMin(),\n 'deal_max_value' => $automatedReport->getDealValueMax(),\n 'call_types' => $automatedReport->getCallTypes(),\n 'call_duration_min_seconds' => $automatedReport->getCallDurationMin(),\n 'call_duration_max_seconds' => $automatedReport->getCallDurationMax(),\n 'special_requirements' => $automatedReport->getAdditionalPromptInput(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->formatReportPeriodName(\n $automatedReport->getFrequency(),\n $fromDate,\n $toDate,\n ),\n 'playbook_categories' => $automatedReport->getPlaybookCategories(),\n 'custom_name' => $automatedReport->getCustomName(),\n ];\n }\n\n // $inputPayload - FE payload structure\n public function getActivitiesCountPayload(array $inputPayload): array\n {\n // Use validateAndTransformData to validate and normalize input\n $validatedData = $this->validateAndTransformData($inputPayload);\n $period = $this->calculateFromAndToDatePeriod(\n $validatedData['frequency'],\n Carbon::parse($validatedData['from']),\n Carbon::parse($validatedData['to']),\n );\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n // Create payload similar to getGenerateReportPayload\n return [\n 'team_id' => $validatedData['team_id'],\n 'group_ids' => $validatedData['groups'] ?? [],\n 'report_type' => $validatedData['type'],\n 'from_date' => $fromDate->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->format(DateTimeInterface::RFC3339),\n 'call_deal_stage' => $validatedData['deal_at_call_stages'] ?? [],\n 'current_deal_stage' => $validatedData['current_deal_stages'] ?? [],\n 'deal_min_value' => $validatedData['deal_value_min'] ?? null,\n 'deal_max_value' => $validatedData['deal_value_max'] ?? null,\n 'call_types' => $validatedData['call_types'],\n 'call_duration_min_seconds' => $validatedData['call_duration_min'] ?? null,\n 'call_duration_max_seconds' => $validatedData['call_duration_max'] ?? null,\n 'special_requirements' => $validatedData['additional_prompt_input'] ?? null,\n 'playbook_categories' => $validatedData['playbook_categories'] ?? [],\n 'request_id' => null,\n 'callback_url' => null,\n ];\n }\n\n public function shouldSendReport(array $users, ?CarbonInterface $generatedAt = null): bool\n {\n if (empty($users)) {\n return false;\n }\n\n $earliestTz = collect($users)\n ->mapWithKeys(function (array $user) {\n $tz = new DateTimeZone($user['timezone']);\n $nowUtc = new DateTime('now', new DateTimeZone('UTC'));\n $offset = $tz->getOffset($nowUtc);\n\n return [$user['timezone'] => $offset];\n })\n ->sortDesc()\n ->keys()\n ->first();\n\n $now = Carbon::now($earliestTz);\n $isScheduledTime = (int) $now->format('H') === self::SENT_REPORT_AT_HOURS;\n\n if ($isScheduledTime) {\n return true;\n }\n\n return $this->hasPassedScheduledTime($generatedAt, $earliestTz);\n }\n\n public function hasPassedScheduledTime(?CarbonInterface $generatedAt, string $timezone): bool\n {\n if ($generatedAt === null) {\n return false;\n }\n\n $now = Carbon::now($timezone);\n $scheduledTime = $now->copy()->setTime(self::SENT_REPORT_AT_HOURS, 0, 0);\n\n if ($now->hour < self::SENT_REPORT_AT_HOURS) {\n $scheduledTime = $scheduledTime->subDay();\n }\n\n $scheduledTimeUtc = $scheduledTime->copy()->utc();\n $generatedAtUtc = $generatedAt->copy()->utc();\n $nowUtc = $now->copy()->utc();\n\n return $generatedAtUtc->lt($scheduledTimeUtc) && $nowUtc->gt($scheduledTimeUtc);\n }\n\n public function calculateFromAndToDatePeriod(\n string $frequency,\n ?Carbon $fromDate = null,\n ?Carbon $toDate = null,\n DateTimeZone|string|null $timezone = 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($timezone);\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()->subWeek()->startOfWeek(CarbonInterface::MONDAY),\n 'toDate' => $now->copy()->subWeek()->endOfWeek(CarbonInterface::SUNDAY),\n ],\n self::FREQUENCY_MONTHLY => [\n 'fromDate' => $now->copy()->subMonthNoOverflow()->startOfMonth(),\n 'toDate' => $now->copy()->subMonthNoOverflow()->endOfMonth(),\n ],\n self::FREQUENCY_QUARTERLY => [\n 'fromDate' => $now->copy()->subQuarterNoOverflow()->startOfQuarter(),\n 'toDate' => $now->copy()->subQuarterNoOverflow()->endOfQuarter(),\n ],\n default => throw new InvalidArgumentException(\"Unsupported frequency: {$frequency}\"),\n };\n }\n\n private function calculateFromAndToDate(AutomatedReport $automatedReport): array\n {\n return $this->calculateFromAndToDatePeriod(\n $automatedReport->getFrequency(),\n $automatedReport->getFrom(),\n $automatedReport->getTo()\n );\n }\n\n public function getAskJiminnyGenerateReportPayload(\n AutomatedReport $automatedReport,\n AutomatedReportResult $reportResult,\n array $activityIds,\n ): array {\n return [\n 'user_question' => $automatedReport->getAskAnythingPrompt()?->getContent(),\n 'call_ids' => array_map('strval', $activityIds),\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResult->getUuid(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->getReportPeriodName($reportResult),\n 'report_name' => $automatedReport->getCustomName(),\n ];\n }\n\n private function getCallbackUrl(): string\n {\n return $this->webhookService->route('jiminny.webhook.reports.ready');\n }\n\n /**\n * Validate and transform payload data for automated reports\n *\n * @param array $data\n *\n * @throws InvalidArgumentException\n *\n * @return array\n */\n private function validateAndTransformData(array $data): array\n {\n // Validate organization (team) and check feature\n $team = $this->validateOrganization($data['organization'] ?? null);\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $type = $this->validateReportType($data['report_type'] ?? null);\n $frequency = $this->validateFrequency($data['frequency'] ?? null);\n $additionalPromptInput = $this->validateAdditionalPromptInput(\n $data['additional_prompt_input'] ?? null\n );\n $customReportName = $this->validateCustomReportName($data['custom_name'] ?? null);\n\n // Prepare data for the database\n $reportData = [\n 'team_id' => $team->getId(),\n 'type' => $type,\n 'status' => $status,\n 'frequency' => $frequency,\n 'additional_prompt_input' => $additionalPromptInput,\n 'custom_name' => $customReportName,\n ];\n\n // Validate deal values\n $reportData = $this->validateDealValues($data, $reportData);\n\n // Validate date range\n $reportData = $this->validateDateRange($data, $reportData, $frequency);\n\n // Validate call durations\n $reportData = $this->validateCallDurations($data, $reportData);\n\n // Validate call types\n $reportData = $this->validateCallTypes($data, $reportData);\n\n // Validate media types\n $reportData = $this->validateMediaTypes($data, $reportData);\n\n // Validate groups\n if (isset($data['teams'])) {\n $reportData['groups'] = $this->validateAndGetGroupIds($team, $data['teams']);\n }\n\n // Validate deal stages\n $reportData = $this->validateDealStages($data, $reportData, $team, $type);\n\n // Validate playbook categories\n $reportData = $this->validatePlaybookCategories($data, $reportData, $team);\n\n // Validate recipients\n $reportData['recipients'] = [\n 'users' => $this->validateAndGetUserIdsByTeam($team, $data['recipients'] ?? []),\n ];\n\n if (isset($data['jiminny_recipients'])) {\n // Validate Jiminny recipients\n $reportData['jiminny_recipients'] = [\n 'users' => $this->validateAndGetJiminnyUserIds((array) $data['jiminny_recipients']),\n ];\n }\n\n return $reportData;\n }\n\n private function validateDealValues(array $data, array $reportData): array\n {\n if (isset($data['min_deal_value'])) {\n $reportData['deal_value_min'] = (int) $data['min_deal_value'];\n\n if ($reportData['deal_value_min'] > 4294967295 || $reportData['deal_value_min'] < 0) {\n throw new InvalidArgumentException('Min deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_deal_value'])) {\n $reportData['deal_value_max'] = (int) $data['max_deal_value'];\n\n if ($reportData['deal_value_max'] > 4294967295 || $reportData['deal_value_max'] < 0) {\n throw new InvalidArgumentException('Max deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['min_deal_value'], $data['max_deal_value'])\n && $data['min_deal_value'] > $data['max_deal_value']\n ) {\n throw new InvalidArgumentException('Min deal value cannot be greater than max deal value');\n }\n\n return $reportData;\n }\n\n private function validateDateRange(array $data, array $reportData, string $frequency): array\n {\n // Set date range only for one_off frequency\n if ($frequency === 'one_off') {\n if (isset($data['start_date_period'])) {\n $reportData['from'] = $this->parseDate($data['start_date_period']);\n }\n\n if (isset($data['end_date_period'])) {\n $reportData['to'] = $this->parseDate($data['end_date_period']);\n }\n\n if (empty($reportData['from']) || empty($reportData['to'])) {\n throw new InvalidArgumentException(\n 'Start date and end date are required for one_off frequency'\n );\n }\n } else {\n $reportData['from'] = null;\n $reportData['to'] = null;\n }\n\n return $reportData;\n }\n\n private function validateCallDurations(array $data, array $reportData): array\n {\n // Convert call durations from minutes to seconds\n if (isset($data['min_call_duration'])) {\n $reportData['call_duration_min'] = (int) $data['min_call_duration'] * 60;\n\n if ($reportData['call_duration_min'] > 4294967295 || $reportData['call_duration_min'] < 0) {\n throw new InvalidArgumentException('Min call duration should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_call_duration'])) {\n $reportData['call_duration_max'] = (int) $data['max_call_duration'] * 60;\n\n if ($reportData['call_duration_max'] > 4294967295 || $reportData['call_duration_max'] < 0) {\n throw new InvalidArgumentException('Max call duration should be between 0 and 4294967295');\n }\n }\n\n return $reportData;\n }\n\n private function validateCallTypes(array $data, array $reportData): array\n {\n // Set call types\n $reportData['call_types'] = $data['call_type'] ?? [];\n if (empty($reportData['call_types'])) {\n $reportData['call_types'] = self::getCallTypes();\n }\n\n foreach ($reportData['call_types'] as $callType) {\n if (! in_array($callType, self::getCallTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Call type %s is invalid', $callType));\n }\n }\n\n return $reportData;\n }\n\n private function validateMediaTypes(array $data, array $reportData): array\n {\n // Set media types from input data\n $reportData['media_types'] = $data['media_types'] ?? [];\n\n if (empty($reportData['media_types'])) {\n throw new InvalidArgumentException('Media types are required');\n }\n\n foreach ($reportData['media_types'] as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n throw new InvalidArgumentException(sprintf('Media type %s is invalid', $mediaType));\n }\n }\n\n return $reportData;\n }\n\n private function validateDealStages(array $data, array $reportData, Team $team, string $reportType): array\n {\n // Validate and set deal stages\n if (isset($data['deal_stage_at_call'])) {\n $reportData['deal_at_call_stages'] =\n $this->validateAndGetDealStageIds($team, $data['deal_stage_at_call'], 'Deal stage at call');\n }\n\n if (isset($data['current_deal_stage'])) {\n $reportData['current_deal_stages'] =\n $this->validateAndGetDealStageIds($team, $data['current_deal_stage'], 'Current deal stage');\n }\n\n // Ensure current_deal_stage is not provided for loss_analysis report type\n if ($reportType === self::TYPE_LOSS_ANALYSIS && ! empty($data['current_deal_stage'])) {\n throw new InvalidArgumentException('Current deal stage is not applicable for Loss Analysis reports');\n }\n\n return $reportData;\n }\n\n // transform uuid to id\n private function validatePlaybookCategories(array $data, array $reportData, Team $team): array\n {\n $key = 'playbook_categories';\n\n if (isset($data[$key])) {\n $payloadIds = $data[$key];\n $ids = [];\n\n foreach ($payloadIds as $uuid) {\n $uuid = (string) $uuid;\n\n try {\n $playbookCategory = $this->playbookCategoryRepository->findByUuid($uuid);\n } catch (Throwable $throwable) {\n Log::error(__METHOD__ . ' ' . $throwable->getMessage());\n\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory) {\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory->hasPlaybook()) {\n throw new InvalidArgumentException(sprintf('Playbook category %s has no playbook', $uuid));\n }\n\n if ($playbookCategory->getPlaybook()->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Playbook category %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $ids[] = $playbookCategory->getId();\n }\n\n $reportData[$key] = $ids;\n }\n\n return $reportData;\n }\n\n private function validateReportStatus($status): bool\n {\n if (! in_array($status, [true, false], true)) {\n throw new InvalidArgumentException('Report status is invalid');\n }\n\n return $status;\n }\n\n private function validateReportType($type): string\n {\n if (! in_array($type, self::getTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Report type is invalid: %s', $type));\n }\n\n return $type;\n }\n\n private function validateFrequency($frequency): string\n {\n if (! in_array($frequency, self::getFrequencies(), true)) {\n throw new InvalidArgumentException('Frequency is invalid');\n }\n\n return $frequency;\n }\n\n private function validateAdditionalPromptInput(?string $additionalPromptInput): ?string\n {\n if ($additionalPromptInput && strlen($additionalPromptInput) > 5000) {\n throw new InvalidArgumentException('Additional Prompt Input should be less than 5000 characters');\n }\n\n return $additionalPromptInput;\n }\n\n private function validateCustomReportName(?string $customReportName): ?string\n {\n if ($customReportName === null || $customReportName === '') {\n return null;\n }\n\n if (strlen($customReportName) > 70) {\n throw new InvalidArgumentException('Custom report name should be less than 70 characters');\n }\n\n return $customReportName;\n }\n\n private function validateOrganization(?string $organizationUuid): Team\n {\n if (! $organizationUuid) {\n throw new InvalidArgumentException('Organization is required');\n }\n\n $team = $this->teamRepository->idOrUuid($organizationUuid);\n\n if (! $team) {\n throw new InvalidArgumentException('Organization not found');\n }\n\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n throw new InvalidArgumentException('Organization does not have the Automated Reports feature');\n }\n\n return $team;\n }\n\n private function validateAndGetGroupIds(Team $team, array $teamUuids): array\n {\n $groupIds = [];\n\n foreach ($teamUuids as $uuid) {\n $group = $this->groupRepository->findByUuid($uuid);\n\n if ($group === null || $group->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Group %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $groupIds[] = $group->getId();\n\n }\n\n return $groupIds;\n }\n\n private function validateAndGetDealStageIds(Team $team, array $stageUuids, string $propertyLabel): array\n {\n $stageIds = [];\n\n foreach ($stageUuids as $uuid) {\n $stage = $this->stageRepository->findByUuid($uuid);\n\n if ($stage === null || $stage->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Stage %s not found for team %s for %s', $uuid, $team->getUuid(), $propertyLabel)\n );\n }\n\n $stageIds[] = $stage->getId();\n }\n\n return $stageIds;\n }\n\n private function validateAndGetUserIds(array $userUuids, callable $teamCheck): array\n {\n if (empty($userUuids)) {\n return [];\n }\n\n $userIds = [];\n\n foreach ($userUuids as $uuid) {\n $user = $this->userRepository->findByUuid($uuid);\n\n if (! $user || ! $user->isStatusActive()) {\n throw new InvalidArgumentException(\n sprintf('User %s not found or is not active', $uuid)\n );\n }\n\n if (! $teamCheck($user)) {\n throw new InvalidArgumentException(\n sprintf('User %s does not belong to the allowed team(s)', $uuid)\n );\n }\n\n $userIds[] = $user->getId();\n }\n\n return $userIds;\n }\n\n private function validateAndGetUserIdsByTeam(Team $team, array $userUuids): array\n {\n return $this->validateAndGetUserIds($userUuids, fn ($user) => $user->getTeamId() === $team->getId());\n }\n\n private function validateAndGetJiminnyUserIds(array $userUuids): array\n {\n $allowedTeamIds = config('kiosk.teamIds', []);\n\n return $this->validateAndGetUserIds($userUuids, fn ($user) => in_array($user->getTeamId(), $allowedTeamIds, true));\n }\n\n private function parseDate(string $dateString): string\n {\n return date('Y-m-d H:i:s', strtotime($dateString));\n }\n\n private function generateReportResultViewUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.view', ['uuid' => $result->getUuid()]);\n }\n\n private function generateReportResultDownloadUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.download', ['uuid' => $result->getUuid()]);\n }\n\n private function getReportMediaRouteResource(AutomatedReportResult $result): string\n {\n if ($result->getMediaType() === self::MEDIA_TYPE_PDF) {\n return self::PDF_KEY;\n } elseif ($result->getMediaType() === self::MEDIA_TYPE_PODCAST) {\n return self::AUDIO_KEY;\n }\n\n throw new \\InvalidArgumentException('Unknown media type.');\n }\n\n public function getMediaPath(AutomatedReportResult $result): ?string\n {\n $url = match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => $result->getPdfUrl(),\n self::MEDIA_TYPE_PODCAST => $result->getPodcastAudioUrl(),\n default => null,\n };\n\n if ($url === null) {\n return null;\n }\n\n $path = parse_url(trim($url, '\"\\''), PHP_URL_PATH);\n\n return $path ?: null;\n }\n\n public function getFilenameSuffix(AutomatedReportResult $result): ?string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => 'Podcast',\n default => null,\n };\n }\n\n public function getMailSubjectSuffix(AutomatedReportResult $result): string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => 'report',\n self::MEDIA_TYPE_PODCAST => 'podcast',\n default => '',\n };\n }\n\n public function getMediaTypeMetadata(AutomatedReportResult $result): array\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => ['extension' => 'mp3', 'mime' => 'audio/mpeg'],\n self::MEDIA_TYPE_PDF => ['extension' => 'pdf', 'mime' => 'application/pdf'],\n default => ['extension' => null, 'mime' => null],\n };\n }\n\n public function deleteS3Files(AutomatedReportResult $result): void\n {\n $teamUuid = $result->getReport()->getTeam()->getUuid();\n $reportUuid = $result->getUuid();\n\n // delete all files for a report uuid no mather of pdf, podcast, or both\n // in case of both - the podcast files are linked to the pdf (parent) uuid\n // pdf and podcast date times should be close\n $path = sprintf('%s/%s/%s', $teamUuid, self::S3_DIR, $reportUuid);\n\n foreach (self::FILE_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted S3 file', [\n 'path' => $file,\n ]);\n }\n }\n\n foreach (self::FILE_PODCAST_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '_podcast.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted Podcast S3 file', [\n 'path' => $file,\n ]);\n }\n }\n }\n\n /**\n *\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): Collection\n {\n return $this->automatedReportsRepository->getTeamIdsWithReportsResults($teamId);\n }\n\n /**\n * Core delete logic for report results using a query\n *\n * @param Builder $query\n * @param array $logContext\n *\n * @return int\n */\n private function deleteReportResultsByQuery(Builder $query, array $logContext = []): int\n {\n $deletedCount = 0;\n\n if ($query->exists()) {\n Log::info(\n 'Run delete report results',\n array_merge(\n $logContext,\n [\n 'service' => 'AutomatedReportsService',\n ]\n )\n );\n\n $query->chunkById(50, function ($results) use (&$deletedCount, $logContext) {\n foreach ($results as $result) {\n $this->deleteReportResult($result);\n $deletedCount++;\n\n Log::info(\n 'Deleted a report result',\n array_merge(\n $logContext,\n [\n 'result_id' => $result->getId(),\n 'report_id' => $result->getReportId(),\n ]\n )\n );\n }\n });\n }\n\n return $deletedCount;\n }\n\n /**\n * Delete report results for a team by retention period\n *\n * @param Team $team\n * @param CarbonImmutable $retentionDate\n *\n * @return int Number of deleted report results\n */\n public function deleteReportsResultsInRetentionPeriod(Team $team, CarbonImmutable $retentionDate): int\n {\n $reportIds = $this->automatedReportsRepository->getReportIdsByTeam($team);\n\n if ($reportIds->isEmpty()) {\n return 0;\n }\n\n $query = $this->automatedReportsRepository\n ->getReportResultsQueryForRetention($team, $retentionDate);\n\n return $this->deleteReportResultsByQuery($query, [\n 'team_id' => $team->getId(),\n 'retention_date' => $retentionDate->toDateTimeString(),\n ]);\n }\n\n /**\n * Delete ALL report results for a specific automated report\n *\n * @param string $uuid\n *\n * @return int\n */\n public function deleteReportResults(string $uuid): int\n {\n $report = $this->getReport($uuid);\n\n $query = $this->automatedReportsRepository->getResultsByReportQuery($report);\n\n return $this->deleteReportResultsByQuery($query, [\n 'report_uuid' => $uuid,\n 'report_id' => $report->getId(),\n ]);\n }\n\n public function deleteReportResult(AutomatedReportResult $result): void\n {\n $this->deleteS3Files($result);\n\n $result->delete();\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getTeamReports(Team $team): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getReportsByTeam($team);\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getReportResults(AutomatedReport $report): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getResultsByReport($report);\n }\n\n public function deleteAllReportResults(AutomatedReport $report): void\n {\n $results = $this->getReportResults($report);\n\n /** @var AutomatedReportResult $result */\n foreach ($results as $result) {\n Log::info('Deleting result', [\n 'report' => $report->getId(),\n 'result' => $result->getId(),\n ]);\n\n $this->deleteReportResult($result);\n }\n }\n\n public function deleteAllData(Team $team): void\n {\n Log::info('Deleting automated report and results for team', [\n 'team' => $team->getId(),\n ]);\n\n $reports = $this->getTeamReports($team);\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n Log::info('Deleting report', [\n 'team' => $team->getId(),\n 'report' => $report->getId(),\n ]);\n\n $this->deleteAllReportResults($report);\n\n $report->delete();\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"21","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"18","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"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 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 * 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 `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\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;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","depth":4,"on_screen":true,"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 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 * 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 `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\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;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Socket fail to connect to host:address=(host=localhost)(port=3306)(type=primary). Connection refused","depth":3,"bounds":{"left":0.3263889,"top":0.0,"width":0.6125,"height":0.018888889},"on_screen":true,"value":"Socket fail to connect to host:address=(host=localhost)(port=3306)(type=primary). Connection refused","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1751808696220340334
|
1126710648141684156
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskAnythingPromptServiceTest
Run 'AskAnythingPromptServiceTest'
Debug 'AskAnythingPromptServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
100
3
34
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Kiosk\AutomatedReports;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Carbon\Exceptions\InvalidFormatException;
use DateTime;
use DateTimeInterface;
use DateTimeZone;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\ActivitySearch\FilterDefinition\InputTypeEnum;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Partner;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Throwable;
class AutomatedReportsService
{
public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';
public const string TYPE_ASK_JIMINNY = 'ask_jiminny';
/**
* Standard report types (used by kiosk for existing automated reports).
*/
// @TODO this will add filter, however if we need to control feature by FF we need conditional logic
public const array TYPES = [
['id' => 'exec_summary', 'name' => 'Exec Summary'],
['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],
['id' => 'product_feedback', 'name' => 'Product Feedback'],
['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],
// ['id' => 'questions', 'name' => 'Questions'],
// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],
];
public const array ALL_TYPES = [
...self::TYPES,
['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],
];
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';
/**
* Frequencies for standard (non-Ask Jiminny) reports.
*/
public const array FREQUENCIES = [
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
/**
* Frequencies for Ask Jiminny reports.
*/
public const array ASK_JIMINNY_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
];
public const string MEDIA_TYPE_PDF = 'pdf';
public const string MEDIA_TYPE_PODCAST = 'podcast';
public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];
public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];
public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];
public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];
public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];
public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];
public const int SENT_REPORT_AT_HOURS = 5;
public const string PDF_KEY = 'pdf';
public const string AUDIO_KEY = 'audio';
private const array ALL_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
private const string S3_DIR = 'reports';
private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];
private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly GroupRepository $groupRepository,
private readonly UserRepository $userRepository,
private readonly StageRepository $stageRepository,
private readonly DealStagesService $dealStagesService,
private readonly RecipientsService $recipientsService,
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly Webhook $webhookService,
private readonly BusDispatcher $dispatcher,
private readonly ActivityTypeService $activityTypeService,
private readonly PlaybookCategoryRepository $playbookCategoryRepository,
private readonly AskAnythingPromptService $askAnythingPromptService,
private readonly SearchRepository $activitySearchRepository,
private readonly AskAnythingRepository $askAnythingRepository,
) {
}
public static function getTypes(): array
{
$types = self::TYPES;
return array_map(static function ($type) {
return $type['id'];
}, $types);
}
public static function getCallTypes(): array
{
return array_map(static function ($callType) {
return $callType['id'];
}, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);
}
public static function getFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::FREQUENCIES);
}
// front-facing structure
public function getReportEnabledFieldData(bool $value = false): array
{
return [
'id' => 'report_enabled',
'label' => '',
'inputType' => InputTypeEnum::TOGGLE,
'value' => $value,
];
}
// Organizations = Teams
public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array
{
$options = $this->getTeams(partner: $partner);
if ($shortVersion) {
return [
'id' => 'organization',
'label' => 'Organization',
'options' => $options,
];
}
return [
'id' => 'organization',
'label' => 'Organization',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $options,
'value' => $value,
'dependencies' => [
'teams',
'deal_stage_at_call',
'current_deal_stage',
'recipients',
ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,
],
'dependsOn' => [],
];
}
// Teams = Groups
public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array
{
if ($shortVersion) {
return [
'id' => 'teams',
'label' => 'Team',
'options' => $options,
];
}
return [
'id' => 'teams',
'label' => 'Team',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => false,
'placeholder' => 'Select',
'options' => $options,
'value' => $value, // value should be an array of objects {id, name}
'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],
'dependsOn' => [],
];
}
public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array
{
$types = [];
if ($team instanceof Team) {
if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
$types = self::TYPES;
}
if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {
$types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];
}
} else {
$types = self::TYPES;
}
if ($shortVersion) {
return [
'id' => 'report_type',
'label' => 'Report Type',
'options' => $types,
];
}
return [
'id' => 'report_type',
'label' => 'Report Type',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $types,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getFrequencyFieldData(?string $value = null): array
{
return [
'id' => 'frequency',
'label' => 'Frequency',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => self::FREQUENCIES,
'value' => $value,
'dependencies' => ['period'],
'dependsOn' => [],
];
}
public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array
{
return [
'id' => 'period',
'label' => 'Select one-off period',
'inputType' => InputTypeEnum::DATE_RANGE,
'required' => true,
'placeholder' => 'Select',
'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],
'queryParams' => [
'startDate' => 'start_date_period',
'endDate' => 'end_date_period',
],
'dependencies' => [],
'dependsOn' => ['frequency'],
];
}
public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array
{
return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);
}
public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);
}
public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);
}
public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'deal_value',
'label' => 'Deal Value',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_deal_value',
'max' => 'max_deal_value',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array
{
$value = [];
$conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;
$dialerOn && $value[] = self::CALL_TYPE_DIALER;
return [
'id' => 'call_type',
'label' => 'Call Type',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => [
self::CALL_TYPE_CONFERENCE,
self::CALL_TYPE_DIALER,
],
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getMediaTypeFieldData(?AutomatedReport $report = null): array
{
$value = [];
if ($report) {
$value = $this->transformMediaTypes($report);
}
return [
'id' => 'media_types',
'label' => 'Export as',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => self::MEDIA_TYPE_OBJECTS,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'call_duration',
'label' => 'Call Duration',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_call_duration',
'max' => 'max_call_duration',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getRecipientsFieldData(?Team $team = null, array $value = []): array
{
return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);
}
public function getJiminnyRecipientsFieldData(array $value = []): array
{
return $this->recipientsService->getJiminnyRecipientsFieldData($value);
}
public function getAdditionalPromptInputFieldData(?string $value = null): array
{
return [
'id' => 'additional_prompt_input',
'label' => 'Special requirements',
'inputType' => InputTypeEnum::TEXTAREA,
'required' => false,
'placeholder' => 'What should be the focus of the report?',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCustomReportNameFieldData(?string $value = null): array
{
return [
'id' => 'custom_name',
'label' => 'Custom report name',
'inputType' => InputTypeEnum::TEXT,
'required' => false,
'placeholder' => 'Enter custom name',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
// data providers
public function getTeams(?Partner $partner = null): array
{
$teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);
$teamData = [];
foreach ($teams as $team) {
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
continue;
}
$teamData[] = $this->transformTeam($team);
}
return $teamData;
}
public function getTeamGroups(string $teamUuid): array
{
$data = [];
$team = $this->getTeam($teamUuid);
if ($team !== null) {
$groups = $team->groups()->get();
foreach ($groups as $group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
];
}
}
return $data;
}
public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array
{
$data = [];
$teams = $this->getTeams(partner: $partner);
foreach ($teams as $team) {
if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {
continue;
}
$data[] = [
'label' => $team['name'],
'groups' => $this->getTeamGroups($team['id']),
];
}
return $data;
}
public function getTeam(string $teamUuid): ?Team
{
return $this->teamRepository->idOrUuid($teamUuid);
}
public function getTeamById(int $teamId): ?Team
{
return $this->teamRepository->find($teamId);
}
public function getGroupsUuids(AutomatedReport $report): array
{
$uuids = [];
$reportGroups = $report->getGroups();
foreach ($reportGroups as $groupId) {
if ($group = $this->groupRepository->find($groupId)) {
$uuids[] = $group->getUuid();
}
}
return $uuids;
}
public function getPlaybookCategoriesUuids(AutomatedReport $report): array
{
$uuids = [];
$playbookCategories = $report->getPlaybookCategories();
foreach ($playbookCategories as $id) {
if ($category = $this->playbookCategoryRepository->find($id)) {
$uuids[] = $category->getUuid();
}
}
return $uuids;
}
public function getDealAtCallStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getDealAtCallStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getCurrentDealStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getCurrentDealStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getRecipients());
}
public function getJiminnyUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function extractUserUuids(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => $user->getUuid())
->values()
->all();
}
// get mail data
public function getRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getRecipients());
}
/**
* @return array<UserContract>
*/
public function getRecipientUserObjects(AutomatedReport $report): array
{
$userIds = $report->getRecipients()['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->values()
->all();
}
private function getJiminnyRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function buildRecipientUsers(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => [
'email' => $user->getEmailAddress(),
'name' => $user->getName(),
'timezone' => $user->getTimezone()->getName(),
])
->values()
->all();
}
public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array
{
if ($report->isAskJiminnyReport()) {
$recipients = $this->resolveAskJiminnyRecipients($report);
} else {
$recipients = $this->getRecipientUsers($report);
if ($includeJiminny) {
$recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));
}
}
$emails = [];
return array_values(array_filter(
$recipients,
static function ($recipient) use (&$emails) {
if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {
return false;
}
$emails[] = $recipient['email'];
return true;
}
));
}
private function resolveAskJiminnyRecipients(AutomatedReport $report): array
{
$recipients = [];
$creator = $report->getCreator();
if ($creator !== null) {
$recipients[] = [
'email' => $creator->getEmailAddress(),
'name' => $creator->getName(),
'timezone' => $creator->getTimezone()->getName(),
];
}
return array_merge(
$recipients,
$this->buildRecipientUsers($report->getRecipients()),
$this->getGroupRecipientUsers($report),
);
}
private function getGroupRecipientUsers(AutomatedReport $report): array
{
$users = [];
foreach ($report->getGroups() as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group === null) {
continue;
}
foreach ($group->getMembers() as $member) {
$users[] = [
'email' => $member->getEmailAddress(),
'name' => $member->getName(),
'timezone' => $member->getTimezone()->getName(),
];
}
}
return $users;
}
public function getReportTypeName(AutomatedReportResult $report): string
{
$type = $report->getReport()->getType();
$getType = $this->transformReportType($type);
return $getType['name'];
}
public function getReportPeriodName(AutomatedReportResult $report): string
{
$from = $report->getFromDate();
$to = $report->getToDate();
$frequency = $report->getReport()->getFrequency();
if ($from === null || $to === null) {
if (! $report->getReport()->isAskJiminnyReport()) {
$invalidPeriod = $from === null ? 'from' : 'to';
throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);
}
$timezone = $report->getReport()->getCreator()?->getTimezone();
$period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);
$from = $period['fromDate'];
$to = $period['toDate'];
}
return $this->formatReportPeriodName($frequency, $from, $to);
}
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 getReportTeamsName(AutomatedReportResult $report): string
{
$groups = $report->getGroups();
if (empty($groups)) {
return 'All';
}
// Get group names from repository
$groupNames = [];
foreach ($groups as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group) {
$groupNames[] = $group->getName();
}
}
if (count($groupNames) === 1) {
// Single team format
$teamsName = $groupNames[0];
} else {
// Multiple teams format
$teamsName = implode(', ', $groupNames);
}
return $teamsName;
}
public function getReportFileName(AutomatedReportResult $report): string
{
$customName = $report->getReport()->getCustomName();
$periodName = $this->getReportPeriodName($report);
$filenameSuffix = $this->getFilenameSuffix($report);
if ($customName) {
if ($filenameSuffix) {
$customName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$customName} - {$periodName}");
}
$baseName = $this->getReportTypeName($report);
if ($filenameSuffix) {
$baseName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}");
}
public function getReportFileNameWithExtension(AutomatedReportResult $result): string
{
$extension = $this->getMediaTypeMetadata($result)['extension'];
return $this->getReportFileName($result) . '.' . $extension;
}
public function sanitizeFileName(string $fileName): string
{
return str_replace(['/', '\\'], '-', $fileName);
}
public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool
{
$recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);
if (in_array($user->getId(), $recipientIds, true)) {
return true;
}
if ($report->isAskJiminnyReport()) {
$groupId = $user->getGroupId();
if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {
return true;
}
}
return false;
}
public function transformReportResults(Collection $automatedReportResults): array
{
$data = [];
foreach ($automatedReportResults as $automatedReportResult) {
/** @var AutomatedReportResult $automatedReportResult */
$report = $automatedReportResult->getReport();
$createdBy = $report->getCreator();
$creator = [
'id' => $createdBy?->getUuid(),
'name' => $createdBy?->getName(),
'email' => $createdBy?->getEmailAddress(),
'photoUrl' => $createdBy?->getPhotoUrl(),
];
$data[] = [
'id' => $automatedReportResult->getUuid(),
'name' => $automatedReportResult->getName(),
'frequency' => $this->transformFrequency($report->getFrequency()),
'recipients' => $this->buildRecipients($report),
'report_type' => $this->transformReportType($report->getType()),
'media_type' => $automatedReportResult->getMediaType(),
'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),
'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),
'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),
'creator' => $creator,
];
}
return $data;
}
private function buildRecipients(AutomatedReport $report): array
{
$creatorUuid = $report->getCreator()?->getUuid();
$recipients = array_values(array_filter(
$this->transformRecipients($report->getRecipients()),
static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,
));
if (! $report->isAskJiminnyReport()) {
return $recipients;
}
return [
...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),
...$recipients,
];
}
public function hasCallTypeConference(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);
}
public function hasCallTypeDialer(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);
}
// transformers
private function transformTeam(Team $team): array
{
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
return [];
}
return [
'id' => $team->getUuid(),
'name' => $team->getName(),
];
}
private function transformReportFullView(AutomatedReport $report): array
{
$base = $this->transformReportBase($report);
return $report->getType() === self::TYPE_ASK_JIMINNY
? $base + $this->transformAskJiminnyFields($report)
: $base + $this->transformStandardReportFields($report);
}
private function transformReportBase(AutomatedReport $report): array
{
return [
'id' => $report->getUuid(),
'organization' => $this->transformOrganization(team: $report->getTeam()),
'report_type' => $this->transformReportType($report->getType()),
'frequency' => $this->transformFrequency($report->getFrequency()),
];
}
private function transformStandardReportFields(AutomatedReport $report): array
{
$team = $report->getTeam();
return [
'report_enabled' => $report->getStatus(),
'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),
'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),
'deal_value_min' => $report->getDealValueMin(),
'deal_value_max' => $report->getDealValueMax(),
'call_types' => $this->transformCallType($report->getCallTypes()),
'media_types' => $this->transformMediaTypes($report),
'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),
'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),
'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),
'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),
'recipients' => $this->transformRecipients($report->getRecipients()),
'created_by' => $this->transformCreator($report->getCreator()),
'additional_prompt_input' => $report->getAdditionalPromptInput(),
'custom_name' => $report->getCustomName(),
'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),
'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),
'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),
];
}
private function transformAskJiminnyFields(AutomatedReport $report): array
{
$team = $report->getTeam();
$creatorId = $report->getAttribute('created_by');
$explicitUserIds = array_values(array_filter(
$report->getRecipients()['users'] ?? [],
static fn ($id) => $id !== $creatorId
));
return [
'report_name' => $report->getCustomName(),
'enabled' => $report->getStatus(),
'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),
'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),
'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),
'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),
];
}
private function transformOrganization(?Team $team): array
{
return [
'id' => $team?->getUuid(),
'name' => $team?->getName(),
];
}
private function transformReportType(string $type): array
{
foreach (self::ALL_TYPES as $typeItem) {
if ($typeItem['id'] === $type) {
return $typeItem;
}
}
return [];
}
private function transformCallType(array $types): array
{
$result = [];
$callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];
foreach ($types as $type) {
foreach ($callTypes as $callTypeItem) {
if ($callTypeItem['id'] === $type) {
$result[] = $callTypeItem;
break;
}
}
}
return $result;
}
private function transformMediaTypes(AutomatedReport $report): array
{
$values = [];
foreach ($report->getMediaTypes() as $mediaType) {
if (! in_array($mediaType, self::MEDIA_TYPES, true)) {
continue;
}
$values[] = match ($mediaType) {
self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,
self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,
};
}
return $values;
}
private function transformFrequency(string $frequency): array
{
foreach (self::ALL_FREQUENCIES as $frequencyItem) {
if ($frequencyItem['id'] === $frequency) {
return $frequencyItem;
}
}
return [];
}
public function transformDurationToMinutes(?int $duration): ?int
{
if (! $duration) {
return null;
}
return (int) ($duration / 60);
}
private function transformGroups(?Team $team, array $groupsIds): array
{
if (empty($groupsIds) || ! $team) {
return [];
}
$data = [];
foreach ($groupsIds as $groupId) {
$group = $team->groups()->where('id', $groupId)->first();
if ($group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
'photoUrl' => $group->getPhotoUrl(),
];
}
}
return $data;
}
private function transformStages(?Team $team, array $stagesIds): array
{
if (empty($stagesIds) || ! $team) {
return [];
}
$data = [];
foreach ($stagesIds as $stageId) {
$stage = $team->stages()->where('id', $stageId)->first();
if ($stage) {
$data[] = [
'id' => $stage->getUuid(),
'name' => $stage->getName(),
];
}
}
return $data;
}
private function transformRecipients(array $recipients): array
{
$users = [];
foreach ($recipients['users'] ?? [] as $userId) {
$users[] = $this->transformUser($userId);
}
return $users;
}
private function transformCreator(?User $user): ?array
{
if ($user === null) {
return null;
}
return $this->transformUser($user->getId());
}
private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array
{
if ($prompt === null) {
return null;
}
return [
'id' => $prompt->getUuid(),
'name' => $prompt->getTitle(),
];
}
private function transformSafeSearch(?Search $search): ?array
{
if ($search === null) {
return null;
}
return [
'id' => $search->getUuid(),
'name' => $search->getName(),
];
}
private function transformUser(int $userId): array
{
/* @var ?User $user */
$user = $this->userRepository->find($userId);
return [
'id' => $user?->getUuid(),
'name' => $user?->getName(),
'email' => $user?->getEmailAddress(),
'photoUrl' => $user?->getPhotoUrl(),
];
}
public function create(array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$validatedData['created_by'] = auth()->id();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
public function update(string $uuid, array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$report = $this->automatedReportsRepository->findByUuid($uuid);
if (! $report) {
throw new InvalidArgumentException('Report not found');
}
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
/**
* Create an Ask Jiminny report.
*/
public function createAskJiminnyReport(array $data, User $creator): array
{
$validatedData = $this->validateAskJiminnyReportData($data, $creator);
$validatedData['created_by'] = $creator->getId();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
return $this->transformReportFullView($automatedReport);
}
/**
* Update an Ask Jiminny report.
*/
public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array
{
if (! $report->isAskJiminnyReport()) {
throw new InvalidArgumentException('Report is not an Ask Jiminny report');
}
$validatedData = $this->validateAskJiminnyReportData($data, $user);
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
return $this->transformReportFullView($automatedReport);
}
public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array
{
if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {
throw new InvalidArgumentException(
'This report is missing a saved search or prompt. ' .
'Edit the report to complete the setup before enabling it.'
);
}
$this->automatedReportsRepository->update($report, ['status' => $status]);
return $this->transformReportFullView($report->fresh());
}
/**
* Validate and transform data for Ask Jiminny reports.
*/
private function validateAskJiminnyReportData(array $data, User $user): array
{
// Validate name
$name = trim($data['report_name'] ?? '');
if (empty($name)) {
throw new InvalidArgumentException('Report name is required');
}
if (mb_strlen($name) > 50) {
throw new InvalidArgumentException('Report name must be 50 characters or less');
}
// Validate frequency (only daily, weekly, monthly for Ask Jiminny)
$frequency = $data['frequency'] ?? null;
$askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];
if (! in_array($frequency, $askJiminnyFrequencies, true)) {
throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');
}
// Validate expiration date
$expiresAt = $data['expires_on'] ?? null;
if (empty($expiresAt)) {
throw new InvalidArgumentException('Expiration date is required');
}
try {
$expiresAtDate = Carbon::parse($expiresAt);
} catch (InvalidFormatException $e) {
throw new InvalidArgumentException('Expiration date format is invalid');
}
$maxExpiration = Carbon::now()->addYear()->endOfDay();
if ($expiresAtDate->gt($maxExpiration)) {
throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');
}
if ($expiresAtDate->isPast()) {
throw new InvalidArgumentException('Expiration date cannot be in the past');
}
// Validate saved search
$activitySearchId = $data['saved_search'] ?? null;
if (empty($activitySearchId)) {
throw new InvalidArgumentException('Saved search is required');
}
$savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);
if (! $savedSearch) {
throw new InvalidArgumentException('Saved search not found or does not belong to you');
}
// Validate saved prompt
$askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;
if (empty($askAnythingPromptId)) {
throw new InvalidArgumentException('Ask Jiminny prompt is required');
}
$prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);
if (! $prompt) {
throw new InvalidArgumentException('Ask Jiminny prompt not found');
}
// Validate status
$status = $data['enabled'] ?? false;
$recipientUserIds = [$user->getId()];
if (! empty($data['share_users'])) {
$sharedUserIds = $this->validateAndGetUserIdsByTeam(
$user->team,
(array) $data['share_users']
);
$recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);
}
$sharedGroupIds = [];
if (! empty($data['share_teams'])) {
$sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);
}
$recipientUserIds = array_values(array_unique($recipientUserIds));
return [
'team_id' => $user->getTeamId(),
'type' => self::TYPE_ASK_JIMINNY,
'status' => (bool) $status,
'frequency' => $frequency,
'custom_name' => $name,
'activity_search_id' => $savedSearch->getId(),
'ask_anything_prompt_id' => $prompt->getId(),
'expires_at' => $expiresAtDate->toDateString(),
'media_types' => [self::MEDIA_TYPE_PDF],
'call_types' => [],
'recipients' => ['users' => $recipientUserIds],
'groups' => $sharedGroupIds,
];
}
public static function getAskJiminnyFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::ASK_JIMINNY_FREQUENCIES);
}
public function getAskJiminnyReportFilters(User $user): array
{
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
return [
[
'id' => 'prompt',
'label' => 'Prompt',
'options' => $prompts,
],
[
'id' => 'saved_search',
'label' => 'Saved Search',
'options' => $savedSearches,
],
];
}
public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array
{
$team = $user->getTeam();
$userTimezone = $user->getTimezone();
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
$teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [
'id' => $group->getUuid(),
'name' => $group->getName(),
])->values()->all();
$shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];
$sharedTeamsValue = [];
$sharedUsersValue = [];
if ($report) {
$sharedTeamsValue = $this->transformGroups($team, $report->getGroups());
$recipientUserIds = $report->getRecipients()['users'] ?? [];
$creatorId = $report->getAttribute('created_by');
$sharedUserIds = array_values(array_filter(
$recipientUserIds,
static fn ($id) => $id !== $creatorId
));
$sharedUsersValue = collect($sharedUserIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (User $u) => [
'id' => $u->getUuid(),
'name' => $u->getName(),
])
->values()
->all();
}
return [
'fields' => [
[
'id' => 'enabled',
'inputType' => InputTypeEnum::TOGGLE,
'label' => '',
'value' => $report?->getStatus() ?? false,
],
[
'id' => 'report_name',
'inputType' => InputTypeEnum::TEXT,
'label' => 'Name',
'placeholder' => 'Enter name',
'required' => true,
'validation' => ['maxLength' => 50],
'value' => $report?->getCustomName() ?? '',
...
|
58019
|
NULL
|
NULL
|
NULL
|
|
58022
|
2043
|
3
|
2026-05-19T11:27:48.399247+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190068399_m2.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskAnythingPromptServiceTest
Run 'AskAnythingPromptServiceTest'
Debug 'AskAnythingPromptServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
100
3
34
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Kiosk\AutomatedReports;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Carbon\Exceptions\InvalidFormatException;
use DateTime;
use DateTimeInterface;
use DateTimeZone;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\ActivitySearch\FilterDefinition\InputTypeEnum;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Partner;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Throwable;
class AutomatedReportsService
{
public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';
public const string TYPE_ASK_JIMINNY = 'ask_jiminny';
/**
* Standard report types (used by kiosk for existing automated reports).
*/
// @TODO this will add filter, however if we need to control feature by FF we need conditional logic
public const array TYPES = [
['id' => 'exec_summary', 'name' => 'Exec Summary'],
['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],
['id' => 'product_feedback', 'name' => 'Product Feedback'],
['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],
// ['id' => 'questions', 'name' => 'Questions'],
// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],
];
public const array ALL_TYPES = [
...self::TYPES,
['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],
];
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';
/**
* Frequencies for standard (non-Ask Jiminny) reports.
*/
public const array FREQUENCIES = [
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
/**
* Frequencies for Ask Jiminny reports.
*/
public const array ASK_JIMINNY_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
];
public const string MEDIA_TYPE_PDF = 'pdf';
public const string MEDIA_TYPE_PODCAST = 'podcast';
public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];
public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];
public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];
public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];
public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];
public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];
public const int SENT_REPORT_AT_HOURS = 5;
public const string PDF_KEY = 'pdf';
public const string AUDIO_KEY = 'audio';
private const array ALL_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
private const string S3_DIR = 'reports';
private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];
private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly GroupRepository $groupRepository,
private readonly UserRepository $userRepository,
private readonly StageRepository $stageRepository,
private readonly DealStagesService $dealStagesService,
private readonly RecipientsService $recipientsService,
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly Webhook $webhookService,
private readonly BusDispatcher $dispatcher,
private readonly ActivityTypeService $activityTypeService,
private readonly PlaybookCategoryRepository $playbookCategoryRepository,
private readonly AskAnythingPromptService $askAnythingPromptService,
private readonly SearchRepository $activitySearchRepository,
private readonly AskAnythingRepository $askAnythingRepository,
) {
}
public static function getTypes(): array
{
$types = self::TYPES;
return array_map(static function ($type) {
return $type['id'];
}, $types);
}
public static function getCallTypes(): array
{
return array_map(static function ($callType) {
return $callType['id'];
}, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);
}
public static function getFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::FREQUENCIES);
}
// front-facing structure
public function getReportEnabledFieldData(bool $value = false): array
{
return [
'id' => 'report_enabled',
'label' => '',
'inputType' => InputTypeEnum::TOGGLE,
'value' => $value,
];
}
// Organizations = Teams
public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array
{
$options = $this->getTeams(partner: $partner);
if ($shortVersion) {
return [
'id' => 'organization',
'label' => 'Organization',
'options' => $options,
];
}
return [
'id' => 'organization',
'label' => 'Organization',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $options,
'value' => $value,
'dependencies' => [
'teams',
'deal_stage_at_call',
'current_deal_stage',
'recipients',
ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,
],
'dependsOn' => [],
];
}
// Teams = Groups
public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array
{
if ($shortVersion) {
return [
'id' => 'teams',
'label' => 'Team',
'options' => $options,
];
}
return [
'id' => 'teams',
'label' => 'Team',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => false,
'placeholder' => 'Select',
'options' => $options,
'value' => $value, // value should be an array of objects {id, name}
'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],
'dependsOn' => [],
];
}
public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array
{
$types = [];
if ($team instanceof Team) {
if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
$types = self::TYPES;
}
if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {
$types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];
}
} else {
$types = self::TYPES;
}
if ($shortVersion) {
return [
'id' => 'report_type',
'label' => 'Report Type',
'options' => $types,
];
}
return [
'id' => 'report_type',
'label' => 'Report Type',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $types,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getFrequencyFieldData(?string $value = null): array
{
return [
'id' => 'frequency',
'label' => 'Frequency',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => self::FREQUENCIES,
'value' => $value,
'dependencies' => ['period'],
'dependsOn' => [],
];
}
public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array
{
return [
'id' => 'period',
'label' => 'Select one-off period',
'inputType' => InputTypeEnum::DATE_RANGE,
'required' => true,
'placeholder' => 'Select',
'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],
'queryParams' => [
'startDate' => 'start_date_period',
'endDate' => 'end_date_period',
],
'dependencies' => [],
'dependsOn' => ['frequency'],
];
}
public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array
{
return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);
}
public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);
}
public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);
}
public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'deal_value',
'label' => 'Deal Value',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_deal_value',
'max' => 'max_deal_value',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array
{
$value = [];
$conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;
$dialerOn && $value[] = self::CALL_TYPE_DIALER;
return [
'id' => 'call_type',
'label' => 'Call Type',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => [
self::CALL_TYPE_CONFERENCE,
self::CALL_TYPE_DIALER,
],
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getMediaTypeFieldData(?AutomatedReport $report = null): array
{
$value = [];
if ($report) {
$value = $this->transformMediaTypes($report);
}
return [
'id' => 'media_types',
'label' => 'Export as',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => self::MEDIA_TYPE_OBJECTS,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'call_duration',
'label' => 'Call Duration',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_call_duration',
'max' => 'max_call_duration',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getRecipientsFieldData(?Team $team = null, array $value = []): array
{
return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);
}
public function getJiminnyRecipientsFieldData(array $value = []): array
{
return $this->recipientsService->getJiminnyRecipientsFieldData($value);
}
public function getAdditionalPromptInputFieldData(?string $value = null): array
{
return [
'id' => 'additional_prompt_input',
'label' => 'Special requirements',
'inputType' => InputTypeEnum::TEXTAREA,
'required' => false,
'placeholder' => 'What should be the focus of the report?',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCustomReportNameFieldData(?string $value = null): array
{
return [
'id' => 'custom_name',
'label' => 'Custom report name',
'inputType' => InputTypeEnum::TEXT,
'required' => false,
'placeholder' => 'Enter custom name',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
// data providers
public function getTeams(?Partner $partner = null): array
{
$teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);
$teamData = [];
foreach ($teams as $team) {
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
continue;
}
$teamData[] = $this->transformTeam($team);
}
return $teamData;
}
public function getTeamGroups(string $teamUuid): array
{
$data = [];
$team = $this->getTeam($teamUuid);
if ($team !== null) {
$groups = $team->groups()->get();
foreach ($groups as $group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
];
}
}
return $data;
}
public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array
{
$data = [];
$teams = $this->getTeams(partner: $partner);
foreach ($teams as $team) {
if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {
continue;
}
$data[] = [
'label' => $team['name'],
'groups' => $this->getTeamGroups($team['id']),
];
}
return $data;
}
public function getTeam(string $teamUuid): ?Team
{
return $this->teamRepository->idOrUuid($teamUuid);
}
public function getTeamById(int $teamId): ?Team
{
return $this->teamRepository->find($teamId);
}
public function getGroupsUuids(AutomatedReport $report): array
{
$uuids = [];
$reportGroups = $report->getGroups();
foreach ($reportGroups as $groupId) {
if ($group = $this->groupRepository->find($groupId)) {
$uuids[] = $group->getUuid();
}
}
return $uuids;
}
public function getPlaybookCategoriesUuids(AutomatedReport $report): array
{
$uuids = [];
$playbookCategories = $report->getPlaybookCategories();
foreach ($playbookCategories as $id) {
if ($category = $this->playbookCategoryRepository->find($id)) {
$uuids[] = $category->getUuid();
}
}
return $uuids;
}
public function getDealAtCallStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getDealAtCallStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getCurrentDealStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getCurrentDealStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getRecipients());
}
public function getJiminnyUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function extractUserUuids(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => $user->getUuid())
->values()
->all();
}
// get mail data
public function getRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getRecipients());
}
/**
* @return array<UserContract>
*/
public function getRecipientUserObjects(AutomatedReport $report): array
{
$userIds = $report->getRecipients()['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->values()
->all();
}
private function getJiminnyRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function buildRecipientUsers(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => [
'email' => $user->getEmailAddress(),
'name' => $user->getName(),
'timezone' => $user->getTimezone()->getName(),
])
->values()
->all();
}
public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array
{
if ($report->isAskJiminnyReport()) {
$recipients = $this->resolveAskJiminnyRecipients($report);
} else {
$recipients = $this->getRecipientUsers($report);
if ($includeJiminny) {
$recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));
}
}
$emails = [];
return array_values(array_filter(
$recipients,
static function ($recipient) use (&$emails) {
if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {
return false;
}
$emails[] = $recipient['email'];
return true;
}
));
}
private function resolveAskJiminnyRecipients(AutomatedReport $report): array
{
$recipients = [];
$creator = $report->getCreator();
if ($creator !== null) {
$recipients[] = [
'email' => $creator->getEmailAddress(),
'name' => $creator->getName(),
'timezone' => $creator->getTimezone()->getName(),
];
}
return array_merge(
$recipients,
$this->buildRecipientUsers($report->getRecipients()),
$this->getGroupRecipientUsers($report),
);
}
private function getGroupRecipientUsers(AutomatedReport $report): array
{
$users = [];
foreach ($report->getGroups() as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group === null) {
continue;
}
foreach ($group->getMembers() as $member) {
$users[] = [
'email' => $member->getEmailAddress(),
'name' => $member->getName(),
'timezone' => $member->getTimezone()->getName(),
];
}
}
return $users;
}
public function getReportTypeName(AutomatedReportResult $report): string
{
$type = $report->getReport()->getType();
$getType = $this->transformReportType($type);
return $getType['name'];
}
public function getReportPeriodName(AutomatedReportResult $report): string
{
$from = $report->getFromDate();
$to = $report->getToDate();
$frequency = $report->getReport()->getFrequency();
if ($from === null || $to === null) {
if (! $report->getReport()->isAskJiminnyReport()) {
$invalidPeriod = $from === null ? 'from' : 'to';
throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);
}
$timezone = $report->getReport()->getCreator()?->getTimezone();
$period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);
$from = $period['fromDate'];
$to = $period['toDate'];
}
return $this->formatReportPeriodName($frequency, $from, $to);
}
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 getReportTeamsName(AutomatedReportResult $report): string
{
$groups = $report->getGroups();
if (empty($groups)) {
return 'All';
}
// Get group names from repository
$groupNames = [];
foreach ($groups as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group) {
$groupNames[] = $group->getName();
}
}
if (count($groupNames) === 1) {
// Single team format
$teamsName = $groupNames[0];
} else {
// Multiple teams format
$teamsName = implode(', ', $groupNames);
}
return $teamsName;
}
public function getReportFileName(AutomatedReportResult $report): string
{
$customName = $report->getReport()->getCustomName();
$periodName = $this->getReportPeriodName($report);
$filenameSuffix = $this->getFilenameSuffix($report);
if ($customName) {
if ($filenameSuffix) {
$customName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$customName} - {$periodName}");
}
$baseName = $this->getReportTypeName($report);
if ($filenameSuffix) {
$baseName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}");
}
public function getReportFileNameWithExtension(AutomatedReportResult $result): string
{
$extension = $this->getMediaTypeMetadata($result)['extension'];
return $this->getReportFileName($result) . '.' . $extension;
}
public function sanitizeFileName(string $fileName): string
{
return str_replace(['/', '\\'], '-', $fileName);
}
public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool
{
$recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);
if (in_array($user->getId(), $recipientIds, true)) {
return true;
}
if ($report->isAskJiminnyReport()) {
$groupId = $user->getGroupId();
if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {
return true;
}
}
return false;
}
public function transformReportResults(Collection $automatedReportResults): array
{
$data = [];
foreach ($automatedReportResults as $automatedReportResult) {
/** @var AutomatedReportResult $automatedReportResult */
$report = $automatedReportResult->getReport();
$createdBy = $report->getCreator();
$creator = [
'id' => $createdBy?->getUuid(),
'name' => $createdBy?->getName(),
'email' => $createdBy?->getEmailAddress(),
'photoUrl' => $createdBy?->getPhotoUrl(),
];
$data[] = [
'id' => $automatedReportResult->getUuid(),
'name' => $automatedReportResult->getName(),
'frequency' => $this->transformFrequency($report->getFrequency()),
'recipients' => $this->buildRecipients($report),
'report_type' => $this->transformReportType($report->getType()),
'media_type' => $automatedReportResult->getMediaType(),
'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),
'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),
'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),
'creator' => $creator,
];
}
return $data;
}
private function buildRecipients(AutomatedReport $report): array
{
$creatorUuid = $report->getCreator()?->getUuid();
$recipients = array_values(array_filter(
$this->transformRecipients($report->getRecipients()),
static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,
));
if (! $report->isAskJiminnyReport()) {
return $recipients;
}
return [
...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),
...$recipients,
];
}
public function hasCallTypeConference(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);
}
public function hasCallTypeDialer(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);
}
// transformers
private function transformTeam(Team $team): array
{
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
return [];
}
return [
'id' => $team->getUuid(),
'name' => $team->getName(),
];
}
private function transformReportFullView(AutomatedReport $report): array
{
$base = $this->transformReportBase($report);
return $report->getType() === self::TYPE_ASK_JIMINNY
? $base + $this->transformAskJiminnyFields($report)
: $base + $this->transformStandardReportFields($report);
}
private function transformReportBase(AutomatedReport $report): array
{
return [
'id' => $report->getUuid(),
'organization' => $this->transformOrganization(team: $report->getTeam()),
'report_type' => $this->transformReportType($report->getType()),
'frequency' => $this->transformFrequency($report->getFrequency()),
];
}
private function transformStandardReportFields(AutomatedReport $report): array
{
$team = $report->getTeam();
return [
'report_enabled' => $report->getStatus(),
'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),
'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),
'deal_value_min' => $report->getDealValueMin(),
'deal_value_max' => $report->getDealValueMax(),
'call_types' => $this->transformCallType($report->getCallTypes()),
'media_types' => $this->transformMediaTypes($report),
'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),
'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),
'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),
'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),
'recipients' => $this->transformRecipients($report->getRecipients()),
'created_by' => $this->transformCreator($report->getCreator()),
'additional_prompt_input' => $report->getAdditionalPromptInput(),
'custom_name' => $report->getCustomName(),
'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),
'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),
'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),
];
}
private function transformAskJiminnyFields(AutomatedReport $report): array
{
$team = $report->getTeam();
$creatorId = $report->getAttribute('created_by');
$explicitUserIds = array_values(array_filter(
$report->getRecipients()['users'] ?? [],
static fn ($id) => $id !== $creatorId
));
return [
'report_name' => $report->getCustomName(),
'enabled' => $report->getStatus(),
'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),
'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),
'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),
'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),
];
}
private function transformOrganization(?Team $team): array
{
return [
'id' => $team?->getUuid(),
'name' => $team?->getName(),
];
}
private function transformReportType(string $type): array
{
foreach (self::ALL_TYPES as $typeItem) {
if ($typeItem['id'] === $type) {
return $typeItem;
}
}
return [];
}
private function transformCallType(array $types): array
{
$result = [];
$callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];
foreach ($types as $type) {
foreach ($callTypes as $callTypeItem) {
if ($callTypeItem['id'] === $type) {
$result[] = $callTypeItem;
break;
}
}
}
return $result;
}
private function transformMediaTypes(AutomatedReport $report): array
{
$values = [];
foreach ($report->getMediaTypes() as $mediaType) {
if (! in_array($mediaType, self::MEDIA_TYPES, true)) {
continue;
}
$values[] = match ($mediaType) {
self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,
self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,
};
}
return $values;
}
private function transformFrequency(string $frequency): array
{
foreach (self::ALL_FREQUENCIES as $frequencyItem) {
if ($frequencyItem['id'] === $frequency) {
return $frequencyItem;
}
}
return [];
}
public function transformDurationToMinutes(?int $duration): ?int
{
if (! $duration) {
return null;
}
return (int) ($duration / 60);
}
private function transformGroups(?Team $team, array $groupsIds): array
{
if (empty($groupsIds) || ! $team) {
return [];
}
$data = [];
foreach ($groupsIds as $groupId) {
$group = $team->groups()->where('id', $groupId)->first();
if ($group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
'photoUrl' => $group->getPhotoUrl(),
];
}
}
return $data;
}
private function transformStages(?Team $team, array $stagesIds): array
{
if (empty($stagesIds) || ! $team) {
return [];
}
$data = [];
foreach ($stagesIds as $stageId) {
$stage = $team->stages()->where('id', $stageId)->first();
if ($stage) {
$data[] = [
'id' => $stage->getUuid(),
'name' => $stage->getName(),
];
}
}
return $data;
}
private function transformRecipients(array $recipients): array
{
$users = [];
foreach ($recipients['users'] ?? [] as $userId) {
$users[] = $this->transformUser($userId);
}
return $users;
}
private function transformCreator(?User $user): ?array
{
if ($user === null) {
return null;
}
return $this->transformUser($user->getId());
}
private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array
{
if ($prompt === null) {
return null;
}
return [
'id' => $prompt->getUuid(),
'name' => $prompt->getTitle(),
];
}
private function transformSafeSearch(?Search $search): ?array
{
if ($search === null) {
return null;
}
return [
'id' => $search->getUuid(),
'name' => $search->getName(),
];
}
private function transformUser(int $userId): array
{
/* @var ?User $user */
$user = $this->userRepository->find($userId);
return [
'id' => $user?->getUuid(),
'name' => $user?->getName(),
'email' => $user?->getEmailAddress(),
'photoUrl' => $user?->getPhotoUrl(),
];
}
public function create(array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$validatedData['created_by'] = auth()->id();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
public function update(string $uuid, array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$report = $this->automatedReportsRepository->findByUuid($uuid);
if (! $report) {
throw new InvalidArgumentException('Report not found');
}
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
/**
* Create an Ask Jiminny report.
*/
public function createAskJiminnyReport(array $data, User $creator): array
{
$validatedData = $this->validateAskJiminnyReportData($data, $creator);
$validatedData['created_by'] = $creator->getId();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
return $this->transformReportFullView($automatedReport);
}
/**
* Update an Ask Jiminny report.
*/
public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array
{
if (! $report->isAskJiminnyReport()) {
throw new InvalidArgumentException('Report is not an Ask Jiminny report');
}
$validatedData = $this->validateAskJiminnyReportData($data, $user);
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
return $this->transformReportFullView($automatedReport);
}
public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array
{
if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {
throw new InvalidArgumentException(
'This report is missing a saved search or prompt. ' .
'Edit the report to complete the setup before enabling it.'
);
}
$this->automatedReportsRepository->update($report, ['status' => $status]);
return $this->transformReportFullView($report->fresh());
}
/**
* Validate and transform data for Ask Jiminny reports.
*/
private function validateAskJiminnyReportData(array $data, User $user): array
{
// Validate name
$name = trim($data['report_name'] ?? '');
if (empty($name)) {
throw new InvalidArgumentException('Report name is required');
}
if (mb_strlen($name) > 50) {
throw new InvalidArgumentException('Report name must be 50 characters or less');
}
// Validate frequency (only daily, weekly, monthly for Ask Jiminny)
$frequency = $data['frequency'] ?? null;
$askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];
if (! in_array($frequency, $askJiminnyFrequencies, true)) {
throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');
}
// Validate expiration date
$expiresAt = $data['expires_on'] ?? null;
if (empty($expiresAt)) {
throw new InvalidArgumentException('Expiration date is required');
}
try {
$expiresAtDate = Carbon::parse($expiresAt);
} catch (InvalidFormatException $e) {
throw new InvalidArgumentException('Expiration date format is invalid');
}
$maxExpiration = Carbon::now()->addYear()->endOfDay();
if ($expiresAtDate->gt($maxExpiration)) {
throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');
}
if ($expiresAtDate->isPast()) {
throw new InvalidArgumentException('Expiration date cannot be in the past');
}
// Validate saved search
$activitySearchId = $data['saved_search'] ?? null;
if (empty($activitySearchId)) {
throw new InvalidArgumentException('Saved search is required');
}
$savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);
if (! $savedSearch) {
throw new InvalidArgumentException('Saved search not found or does not belong to you');
}
// Validate saved prompt
$askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;
if (empty($askAnythingPromptId)) {
throw new InvalidArgumentException('Ask Jiminny prompt is required');
}
$prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);
if (! $prompt) {
throw new InvalidArgumentException('Ask Jiminny prompt not found');
}
// Validate status
$status = $data['enabled'] ?? false;
$recipientUserIds = [$user->getId()];
if (! empty($data['share_users'])) {
$sharedUserIds = $this->validateAndGetUserIdsByTeam(
$user->team,
(array) $data['share_users']
);
$recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);
}
$sharedGroupIds = [];
if (! empty($data['share_teams'])) {
$sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);
}
$recipientUserIds = array_values(array_unique($recipientUserIds));
return [
'team_id' => $user->getTeamId(),
'type' => self::TYPE_ASK_JIMINNY,
'status' => (bool) $status,
'frequency' => $frequency,
'custom_name' => $name,
'activity_search_id' => $savedSearch->getId(),
'ask_anything_prompt_id' => $prompt->getId(),
'expires_at' => $expiresAtDate->toDateString(),
'media_types' => [self::MEDIA_TYPE_PDF],
'call_types' => [],
'recipients' => ['users' => $recipientUserIds],
'groups' => $sharedGroupIds,
];
}
public static function getAskJiminnyFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::ASK_JIMINNY_FREQUENCIES);
}
public function getAskJiminnyReportFilters(User $user): array
{
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
return [
[
'id' => 'prompt',
'label' => 'Prompt',
'options' => $prompts,
],
[
'id' => 'saved_search',
'label' => 'Saved Search',
'options' => $savedSearches,
],
];
}
public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array
{
$team = $user->getTeam();
$userTimezone = $user->getTimezone();
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
$teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [
'id' => $group->getUuid(),
'name' => $group->getName(),
])->values()->all();
$shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];
$sharedTeamsValue = [];
$sharedUsersValue = [];
if ($report) {
$sharedTeamsValue = $this->transformGroups($team, $report->getGroups());
$recipientUserIds = $report->getRecipients()['users'] ?? [];
$creatorId = $report->getAttribute('created_by');
$sharedUserIds = array_values(array_filter(
$recipientUserIds,
static fn ($id) => $id !== $creatorId
));
$sharedUsersValue = collect($sharedUserIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (User $u) => [
'id' => $u->getUuid(),
'name' => $u->getName(),
])
->values()
->all();
}
return [
'fields' => [
[
'id' => 'enabled',
'inputType' => InputTypeEnum::TOGGLE,
'label' => '',
'value' => $report?->getStatus() ?? false,
],
[
'id' => 'report_name',
'inputType' => InputTypeEnum::TEXT,
'label' => 'Name',
'placeholder' => 'Enter name',
'required' => true,
'validation' => ['maxLength' => 50],
'value' => $report?->getCustomName() ?? '',
...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.10405585,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects<br/>Some incoming commits are not fetched<br/>","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.8194814,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskAnythingPromptServiceTest","depth":6,"bounds":{"left":0.83477396,"top":0.019952115,"width":0.080784574,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskAnythingPromptServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskAnythingPromptServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"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},"on_screen":true,"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},"on_screen":true,"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},"on_screen":true,"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},"on_screen":true,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"100","depth":4,"bounds":{"left":0.37599733,"top":0.15003991,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.3899601,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"34","depth":4,"bounds":{"left":0.39993352,"top":0.15003991,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.4192154,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"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\\Services\\Kiosk\\AutomatedReports;\n\nuse Carbon\\CarbonImmutable;\nuse Carbon\\CarbonInterface;\nuse Carbon\\Exceptions\\InvalidFormatException;\nuse DateTime;\nuse DateTimeInterface;\nuse DateTimeZone;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\InputTypeEnum;\nuse Jiminny\\Component\\AskAnything\\AskAnythingPromptService;\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Contracts\\Repositories\\PlaybookCategoryRepository;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\ModelNotFoundException;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Partner;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Repositories\\GroupRepository;\nuse Jiminny\\Repositories\\SearchRepository;\nuse Jiminny\\Repositories\\StageRepository;\nuse Throwable;\n\nclass AutomatedReportsService\n{\n public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';\n public const string TYPE_ASK_JIMINNY = 'ask_jiminny';\n\n /**\n * Standard report types (used by kiosk for existing automated reports).\n */\n // @TODO this will add filter, however if we need to control feature by FF we need conditional logic\n public const array TYPES = [\n ['id' => 'exec_summary', 'name' => 'Exec Summary'],\n ['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],\n ['id' => 'product_feedback', 'name' => 'Product Feedback'],\n ['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],\n// ['id' => 'questions', 'name' => 'Questions'],\n// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],\n ];\n\n public const array ALL_TYPES = [\n ...self::TYPES,\n ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],\n ];\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\n /**\n * Frequencies for standard (non-Ask Jiminny) reports.\n */\n public const array FREQUENCIES = [\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n\n /**\n * Frequencies for Ask Jiminny reports.\n */\n public const array ASK_JIMINNY_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ];\n\n public const string MEDIA_TYPE_PDF = 'pdf';\n public const string MEDIA_TYPE_PODCAST = 'podcast';\n public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];\n public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];\n public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];\n public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];\n\n public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];\n public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];\n public const int SENT_REPORT_AT_HOURS = 5;\n public const string PDF_KEY = 'pdf';\n public const string AUDIO_KEY = 'audio';\n\n private const array ALL_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n private const string S3_DIR = 'reports';\n private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];\n private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];\n\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly GroupRepository $groupRepository,\n private readonly UserRepository $userRepository,\n private readonly StageRepository $stageRepository,\n private readonly DealStagesService $dealStagesService,\n private readonly RecipientsService $recipientsService,\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly Webhook $webhookService,\n private readonly BusDispatcher $dispatcher,\n private readonly ActivityTypeService $activityTypeService,\n private readonly PlaybookCategoryRepository $playbookCategoryRepository,\n private readonly AskAnythingPromptService $askAnythingPromptService,\n private readonly SearchRepository $activitySearchRepository,\n private readonly AskAnythingRepository $askAnythingRepository,\n ) {\n }\n\n public static function getTypes(): array\n {\n $types = self::TYPES;\n\n return array_map(static function ($type) {\n return $type['id'];\n }, $types);\n }\n\n public static function getCallTypes(): array\n {\n return array_map(static function ($callType) {\n return $callType['id'];\n }, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);\n }\n\n public static function getFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::FREQUENCIES);\n }\n\n // front-facing structure\n public function getReportEnabledFieldData(bool $value = false): array\n {\n return [\n 'id' => 'report_enabled',\n 'label' => '',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'value' => $value,\n ];\n }\n\n // Organizations = Teams\n public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array\n {\n $options = $this->getTeams(partner: $partner);\n\n if ($shortVersion) {\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value,\n 'dependencies' => [\n 'teams',\n 'deal_stage_at_call',\n 'current_deal_stage',\n 'recipients',\n ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,\n ],\n 'dependsOn' => [],\n ];\n }\n\n // Teams = Groups\n public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array\n {\n if ($shortVersion) {\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value, // value should be an array of objects {id, name}\n 'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],\n 'dependsOn' => [],\n ];\n }\n\n public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array\n {\n $types = [];\n if ($team instanceof Team) {\n if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n $types = self::TYPES;\n }\n if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {\n $types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];\n }\n } else {\n $types = self::TYPES;\n }\n\n if ($shortVersion) {\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'options' => $types,\n ];\n }\n\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $types,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getFrequencyFieldData(?string $value = null): array\n {\n return [\n 'id' => 'frequency',\n 'label' => 'Frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::FREQUENCIES,\n 'value' => $value,\n 'dependencies' => ['period'],\n 'dependsOn' => [],\n ];\n }\n\n public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array\n {\n return [\n 'id' => 'period',\n 'label' => 'Select one-off period',\n 'inputType' => InputTypeEnum::DATE_RANGE,\n 'required' => true,\n 'placeholder' => 'Select',\n 'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],\n 'queryParams' => [\n 'startDate' => 'start_date_period',\n 'endDate' => 'end_date_period',\n ],\n 'dependencies' => [],\n 'dependsOn' => ['frequency'],\n ];\n }\n\n public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array\n {\n return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);\n }\n\n public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);\n }\n\n public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);\n }\n\n public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'deal_value',\n 'label' => 'Deal Value',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_deal_value',\n 'max' => 'max_deal_value',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array\n {\n $value = [];\n $conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;\n $dialerOn && $value[] = self::CALL_TYPE_DIALER;\n\n return [\n 'id' => 'call_type',\n 'label' => 'Call Type',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => [\n self::CALL_TYPE_CONFERENCE,\n self::CALL_TYPE_DIALER,\n ],\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getMediaTypeFieldData(?AutomatedReport $report = null): array\n {\n $value = [];\n\n if ($report) {\n $value = $this->transformMediaTypes($report);\n }\n\n return [\n 'id' => 'media_types',\n 'label' => 'Export as',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => self::MEDIA_TYPE_OBJECTS,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'call_duration',\n 'label' => 'Call Duration',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_call_duration',\n 'max' => 'max_call_duration',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getRecipientsFieldData(?Team $team = null, array $value = []): array\n {\n return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);\n }\n\n public function getJiminnyRecipientsFieldData(array $value = []): array\n {\n return $this->recipientsService->getJiminnyRecipientsFieldData($value);\n }\n\n public function getAdditionalPromptInputFieldData(?string $value = null): array\n {\n return [\n 'id' => 'additional_prompt_input',\n 'label' => 'Special requirements',\n 'inputType' => InputTypeEnum::TEXTAREA,\n 'required' => false,\n 'placeholder' => 'What should be the focus of the report?',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCustomReportNameFieldData(?string $value = null): array\n {\n return [\n 'id' => 'custom_name',\n 'label' => 'Custom report name',\n 'inputType' => InputTypeEnum::TEXT,\n 'required' => false,\n 'placeholder' => 'Enter custom name',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n // data providers\n public function getTeams(?Partner $partner = null): array\n {\n $teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);\n\n $teamData = [];\n foreach ($teams as $team) {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n continue;\n }\n\n $teamData[] = $this->transformTeam($team);\n }\n\n return $teamData;\n }\n\n public function getTeamGroups(string $teamUuid): array\n {\n $data = [];\n $team = $this->getTeam($teamUuid);\n\n if ($team !== null) {\n $groups = $team->groups()->get();\n\n foreach ($groups as $group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array\n {\n $data = [];\n $teams = $this->getTeams(partner: $partner);\n\n foreach ($teams as $team) {\n if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {\n continue;\n }\n\n $data[] = [\n 'label' => $team['name'],\n 'groups' => $this->getTeamGroups($team['id']),\n ];\n }\n\n return $data;\n }\n\n public function getTeam(string $teamUuid): ?Team\n {\n return $this->teamRepository->idOrUuid($teamUuid);\n }\n\n public function getTeamById(int $teamId): ?Team\n {\n return $this->teamRepository->find($teamId);\n }\n\n public function getGroupsUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportGroups = $report->getGroups();\n foreach ($reportGroups as $groupId) {\n if ($group = $this->groupRepository->find($groupId)) {\n $uuids[] = $group->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getPlaybookCategoriesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $playbookCategories = $report->getPlaybookCategories();\n foreach ($playbookCategories as $id) {\n if ($category = $this->playbookCategoryRepository->find($id)) {\n $uuids[] = $category->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getDealAtCallStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getDealAtCallStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getCurrentDealStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getCurrentDealStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getRecipients());\n }\n\n public function getJiminnyUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function extractUserUuids(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => $user->getUuid())\n ->values()\n ->all();\n }\n\n // get mail data\n public function getRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getRecipients());\n }\n\n /**\n * @return array<UserContract>\n */\n public function getRecipientUserObjects(AutomatedReport $report): array\n {\n $userIds = $report->getRecipients()['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->values()\n ->all();\n }\n\n private function getJiminnyRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function buildRecipientUsers(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => [\n 'email' => $user->getEmailAddress(),\n 'name' => $user->getName(),\n 'timezone' => $user->getTimezone()->getName(),\n ])\n ->values()\n ->all();\n }\n\n public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array\n {\n if ($report->isAskJiminnyReport()) {\n $recipients = $this->resolveAskJiminnyRecipients($report);\n } else {\n $recipients = $this->getRecipientUsers($report);\n if ($includeJiminny) {\n $recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));\n }\n }\n\n $emails = [];\n\n return array_values(array_filter(\n $recipients,\n static function ($recipient) use (&$emails) {\n if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {\n return false;\n }\n $emails[] = $recipient['email'];\n\n return true;\n }\n ));\n }\n\n private function resolveAskJiminnyRecipients(AutomatedReport $report): array\n {\n $recipients = [];\n\n $creator = $report->getCreator();\n if ($creator !== null) {\n $recipients[] = [\n 'email' => $creator->getEmailAddress(),\n 'name' => $creator->getName(),\n 'timezone' => $creator->getTimezone()->getName(),\n ];\n }\n\n return array_merge(\n $recipients,\n $this->buildRecipientUsers($report->getRecipients()),\n $this->getGroupRecipientUsers($report),\n );\n }\n\n private function getGroupRecipientUsers(AutomatedReport $report): array\n {\n $users = [];\n foreach ($report->getGroups() as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group === null) {\n continue;\n }\n foreach ($group->getMembers() as $member) {\n $users[] = [\n 'email' => $member->getEmailAddress(),\n 'name' => $member->getName(),\n 'timezone' => $member->getTimezone()->getName(),\n ];\n }\n }\n\n return $users;\n }\n\n public function getReportTypeName(AutomatedReportResult $report): string\n {\n $type = $report->getReport()->getType();\n\n $getType = $this->transformReportType($type);\n\n return $getType['name'];\n }\n\n public function getReportPeriodName(AutomatedReportResult $report): string\n {\n $from = $report->getFromDate();\n $to = $report->getToDate();\n $frequency = $report->getReport()->getFrequency();\n\n if ($from === null || $to === null) {\n if (! $report->getReport()->isAskJiminnyReport()) {\n $invalidPeriod = $from === null ? 'from' : 'to';\n\n throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);\n }\n\n $timezone = $report->getReport()->getCreator()?->getTimezone();\n $period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);\n $from = $period['fromDate'];\n $to = $period['toDate'];\n }\n\n return $this->formatReportPeriodName($frequency, $from, $to);\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 getReportTeamsName(AutomatedReportResult $report): string\n {\n $groups = $report->getGroups();\n\n if (empty($groups)) {\n return 'All';\n }\n\n // Get group names from repository\n $groupNames = [];\n foreach ($groups as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group) {\n $groupNames[] = $group->getName();\n }\n }\n\n if (count($groupNames) === 1) {\n // Single team format\n $teamsName = $groupNames[0];\n } else {\n // Multiple teams format\n $teamsName = implode(', ', $groupNames);\n }\n\n return $teamsName;\n }\n\n public function getReportFileName(AutomatedReportResult $report): string\n {\n $customName = $report->getReport()->getCustomName();\n $periodName = $this->getReportPeriodName($report);\n $filenameSuffix = $this->getFilenameSuffix($report);\n\n if ($customName) {\n if ($filenameSuffix) {\n $customName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$customName} - {$periodName}\");\n }\n\n $baseName = $this->getReportTypeName($report);\n\n if ($filenameSuffix) {\n $baseName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}\");\n }\n\n public function getReportFileNameWithExtension(AutomatedReportResult $result): string\n {\n $extension = $this->getMediaTypeMetadata($result)['extension'];\n\n return $this->getReportFileName($result) . '.' . $extension;\n }\n\n public function sanitizeFileName(string $fileName): string\n {\n return str_replace(['/', '\\\\'], '-', $fileName);\n }\n\n public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool\n {\n $recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);\n if (in_array($user->getId(), $recipientIds, true)) {\n return true;\n }\n\n if ($report->isAskJiminnyReport()) {\n $groupId = $user->getGroupId();\n if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {\n return true;\n }\n }\n\n return false;\n }\n\n public function transformReportResults(Collection $automatedReportResults): array\n {\n $data = [];\n foreach ($automatedReportResults as $automatedReportResult) {\n /** @var AutomatedReportResult $automatedReportResult */\n\n $report = $automatedReportResult->getReport();\n\n $createdBy = $report->getCreator();\n $creator = [\n 'id' => $createdBy?->getUuid(),\n 'name' => $createdBy?->getName(),\n 'email' => $createdBy?->getEmailAddress(),\n 'photoUrl' => $createdBy?->getPhotoUrl(),\n ];\n\n $data[] = [\n 'id' => $automatedReportResult->getUuid(),\n 'name' => $automatedReportResult->getName(),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n 'recipients' => $this->buildRecipients($report),\n 'report_type' => $this->transformReportType($report->getType()),\n 'media_type' => $automatedReportResult->getMediaType(),\n 'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),\n 'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),\n 'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),\n 'creator' => $creator,\n ];\n }\n\n return $data;\n }\n\n private function buildRecipients(AutomatedReport $report): array\n {\n $creatorUuid = $report->getCreator()?->getUuid();\n\n $recipients = array_values(array_filter(\n $this->transformRecipients($report->getRecipients()),\n static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,\n ));\n\n if (! $report->isAskJiminnyReport()) {\n return $recipients;\n }\n\n return [\n ...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),\n ...$recipients,\n ];\n }\n\n public function hasCallTypeConference(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);\n }\n\n public function hasCallTypeDialer(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);\n }\n\n // transformers\n private function transformTeam(Team $team): array\n {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n return [];\n }\n\n return [\n 'id' => $team->getUuid(),\n 'name' => $team->getName(),\n ];\n }\n\n private function transformReportFullView(AutomatedReport $report): array\n {\n $base = $this->transformReportBase($report);\n\n return $report->getType() === self::TYPE_ASK_JIMINNY\n ? $base + $this->transformAskJiminnyFields($report)\n : $base + $this->transformStandardReportFields($report);\n }\n\n private function transformReportBase(AutomatedReport $report): array\n {\n return [\n 'id' => $report->getUuid(),\n 'organization' => $this->transformOrganization(team: $report->getTeam()),\n 'report_type' => $this->transformReportType($report->getType()),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n ];\n }\n\n private function transformStandardReportFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n\n return [\n 'report_enabled' => $report->getStatus(),\n 'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),\n 'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),\n 'deal_value_min' => $report->getDealValueMin(),\n 'deal_value_max' => $report->getDealValueMax(),\n 'call_types' => $this->transformCallType($report->getCallTypes()),\n 'media_types' => $this->transformMediaTypes($report),\n 'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),\n 'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),\n 'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),\n 'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),\n 'recipients' => $this->transformRecipients($report->getRecipients()),\n 'created_by' => $this->transformCreator($report->getCreator()),\n 'additional_prompt_input' => $report->getAdditionalPromptInput(),\n 'custom_name' => $report->getCustomName(),\n 'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),\n 'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),\n 'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),\n ];\n }\n\n private function transformAskJiminnyFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n $creatorId = $report->getAttribute('created_by');\n $explicitUserIds = array_values(array_filter(\n $report->getRecipients()['users'] ?? [],\n static fn ($id) => $id !== $creatorId\n ));\n\n return [\n 'report_name' => $report->getCustomName(),\n 'enabled' => $report->getStatus(),\n 'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),\n 'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),\n 'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),\n 'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),\n ];\n }\n\n private function transformOrganization(?Team $team): array\n {\n return [\n 'id' => $team?->getUuid(),\n 'name' => $team?->getName(),\n ];\n }\n\n private function transformReportType(string $type): array\n {\n foreach (self::ALL_TYPES as $typeItem) {\n if ($typeItem['id'] === $type) {\n return $typeItem;\n }\n }\n\n return [];\n }\n\n private function transformCallType(array $types): array\n {\n $result = [];\n $callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];\n\n foreach ($types as $type) {\n foreach ($callTypes as $callTypeItem) {\n if ($callTypeItem['id'] === $type) {\n $result[] = $callTypeItem;\n\n break;\n }\n }\n }\n\n return $result;\n }\n\n private function transformMediaTypes(AutomatedReport $report): array\n {\n $values = [];\n\n foreach ($report->getMediaTypes() as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n continue;\n }\n\n $values[] = match ($mediaType) {\n self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,\n self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,\n };\n }\n\n return $values;\n }\n\n private function transformFrequency(string $frequency): array\n {\n foreach (self::ALL_FREQUENCIES as $frequencyItem) {\n if ($frequencyItem['id'] === $frequency) {\n return $frequencyItem;\n }\n }\n\n return [];\n }\n\n public function transformDurationToMinutes(?int $duration): ?int\n {\n if (! $duration) {\n return null;\n }\n\n return (int) ($duration / 60);\n }\n\n private function transformGroups(?Team $team, array $groupsIds): array\n {\n if (empty($groupsIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($groupsIds as $groupId) {\n $group = $team->groups()->where('id', $groupId)->first();\n\n if ($group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n 'photoUrl' => $group->getPhotoUrl(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformStages(?Team $team, array $stagesIds): array\n {\n if (empty($stagesIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($stagesIds as $stageId) {\n $stage = $team->stages()->where('id', $stageId)->first();\n\n if ($stage) {\n $data[] = [\n 'id' => $stage->getUuid(),\n 'name' => $stage->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformRecipients(array $recipients): array\n {\n $users = [];\n foreach ($recipients['users'] ?? [] as $userId) {\n $users[] = $this->transformUser($userId);\n }\n\n return $users;\n }\n\n private function transformCreator(?User $user): ?array\n {\n if ($user === null) {\n return null;\n }\n\n return $this->transformUser($user->getId());\n }\n\n private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array\n {\n if ($prompt === null) {\n return null;\n }\n\n return [\n 'id' => $prompt->getUuid(),\n 'name' => $prompt->getTitle(),\n ];\n }\n\n private function transformSafeSearch(?Search $search): ?array\n {\n if ($search === null) {\n return null;\n }\n\n return [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ];\n }\n\n private function transformUser(int $userId): array\n {\n /* @var ?User $user */\n $user = $this->userRepository->find($userId);\n\n return [\n 'id' => $user?->getUuid(),\n 'name' => $user?->getName(),\n 'email' => $user?->getEmailAddress(),\n 'photoUrl' => $user?->getPhotoUrl(),\n ];\n }\n\n public function create(array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $validatedData['created_by'] = auth()->id();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function update(string $uuid, array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $report = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $report) {\n throw new InvalidArgumentException('Report not found');\n }\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Create an Ask Jiminny report.\n */\n public function createAskJiminnyReport(array $data, User $creator): array\n {\n $validatedData = $this->validateAskJiminnyReportData($data, $creator);\n $validatedData['created_by'] = $creator->getId();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Update an Ask Jiminny report.\n */\n public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array\n {\n if (! $report->isAskJiminnyReport()) {\n throw new InvalidArgumentException('Report is not an Ask Jiminny report');\n }\n\n $validatedData = $this->validateAskJiminnyReportData($data, $user);\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array\n {\n if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {\n throw new InvalidArgumentException(\n 'This report is missing a saved search or prompt. ' .\n 'Edit the report to complete the setup before enabling it.'\n );\n }\n\n $this->automatedReportsRepository->update($report, ['status' => $status]);\n\n return $this->transformReportFullView($report->fresh());\n }\n\n /**\n * Validate and transform data for Ask Jiminny reports.\n */\n private function validateAskJiminnyReportData(array $data, User $user): array\n {\n // Validate name\n $name = trim($data['report_name'] ?? '');\n if (empty($name)) {\n throw new InvalidArgumentException('Report name is required');\n }\n if (mb_strlen($name) > 50) {\n throw new InvalidArgumentException('Report name must be 50 characters or less');\n }\n\n // Validate frequency (only daily, weekly, monthly for Ask Jiminny)\n $frequency = $data['frequency'] ?? null;\n $askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];\n if (! in_array($frequency, $askJiminnyFrequencies, true)) {\n throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');\n }\n\n // Validate expiration date\n $expiresAt = $data['expires_on'] ?? null;\n if (empty($expiresAt)) {\n throw new InvalidArgumentException('Expiration date is required');\n }\n\n try {\n $expiresAtDate = Carbon::parse($expiresAt);\n } catch (InvalidFormatException $e) {\n throw new InvalidArgumentException('Expiration date format is invalid');\n }\n $maxExpiration = Carbon::now()->addYear()->endOfDay();\n if ($expiresAtDate->gt($maxExpiration)) {\n throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');\n }\n if ($expiresAtDate->isPast()) {\n throw new InvalidArgumentException('Expiration date cannot be in the past');\n }\n\n // Validate saved search\n $activitySearchId = $data['saved_search'] ?? null;\n if (empty($activitySearchId)) {\n throw new InvalidArgumentException('Saved search is required');\n }\n $savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);\n if (! $savedSearch) {\n throw new InvalidArgumentException('Saved search not found or does not belong to you');\n }\n\n // Validate saved prompt\n $askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;\n if (empty($askAnythingPromptId)) {\n throw new InvalidArgumentException('Ask Jiminny prompt is required');\n }\n $prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);\n if (! $prompt) {\n throw new InvalidArgumentException('Ask Jiminny prompt not found');\n }\n\n // Validate status\n $status = $data['enabled'] ?? false;\n\n $recipientUserIds = [$user->getId()];\n\n if (! empty($data['share_users'])) {\n $sharedUserIds = $this->validateAndGetUserIdsByTeam(\n $user->team,\n (array) $data['share_users']\n );\n $recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);\n }\n\n $sharedGroupIds = [];\n if (! empty($data['share_teams'])) {\n $sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);\n }\n\n $recipientUserIds = array_values(array_unique($recipientUserIds));\n\n return [\n 'team_id' => $user->getTeamId(),\n 'type' => self::TYPE_ASK_JIMINNY,\n 'status' => (bool) $status,\n 'frequency' => $frequency,\n 'custom_name' => $name,\n 'activity_search_id' => $savedSearch->getId(),\n 'ask_anything_prompt_id' => $prompt->getId(),\n 'expires_at' => $expiresAtDate->toDateString(),\n 'media_types' => [self::MEDIA_TYPE_PDF],\n 'call_types' => [],\n 'recipients' => ['users' => $recipientUserIds],\n 'groups' => $sharedGroupIds,\n ];\n }\n\n public static function getAskJiminnyFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::ASK_JIMINNY_FREQUENCIES);\n }\n\n public function getAskJiminnyReportFilters(User $user): array\n {\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n return [\n [\n 'id' => 'prompt',\n 'label' => 'Prompt',\n 'options' => $prompts,\n ],\n [\n 'id' => 'saved_search',\n 'label' => 'Saved Search',\n 'options' => $savedSearches,\n ],\n ];\n }\n\n public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array\n {\n $team = $user->getTeam();\n $userTimezone = $user->getTimezone();\n\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n $teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ])->values()->all();\n\n $shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];\n\n $sharedTeamsValue = [];\n $sharedUsersValue = [];\n if ($report) {\n $sharedTeamsValue = $this->transformGroups($team, $report->getGroups());\n\n $recipientUserIds = $report->getRecipients()['users'] ?? [];\n $creatorId = $report->getAttribute('created_by');\n $sharedUserIds = array_values(array_filter(\n $recipientUserIds,\n static fn ($id) => $id !== $creatorId\n ));\n $sharedUsersValue = collect($sharedUserIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (User $u) => [\n 'id' => $u->getUuid(),\n 'name' => $u->getName(),\n ])\n ->values()\n ->all();\n }\n\n return [\n 'fields' => [\n [\n 'id' => 'enabled',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'label' => '',\n 'value' => $report?->getStatus() ?? false,\n ],\n [\n 'id' => 'report_name',\n 'inputType' => InputTypeEnum::TEXT,\n 'label' => 'Name',\n 'placeholder' => 'Enter name',\n 'required' => true,\n 'validation' => ['maxLength' => 50],\n 'value' => $report?->getCustomName() ?? '',\n ],\n [\n 'id' => 'frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Frequency',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::ASK_JIMINNY_FREQUENCIES,\n 'value' => $report ? $this->transformFrequency($report->getFrequency()) : null,\n ],\n [\n 'id' => 'expires_on',\n 'inputType' => InputTypeEnum::DATE,\n 'label' => 'Expires on',\n 'required' => true,\n 'placeholder' => 'Select',\n 'validation' => [\n 'minDate' => now($userTimezone)->toDateString(),\n 'maxDate' => now($userTimezone)->addYear()->toDateString(),\n ],\n 'value' => $report?->getExpiresAt()?->toDateString(),\n ],\n [\n 'id' => 'share_teams',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team',\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $teamGroups,\n 'value' => $sharedTeamsValue,\n ],\n [\n 'id' => 'share_users',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team member',\n 'required' => false,\n 'placeholder' => 'Select',\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'options' => $shareUsers,\n 'value' => $sharedUsersValue,\n ],\n [\n 'id' => 'saved_search',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Saved search',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $savedSearches,\n 'value' => $report && $report->getSavedSearch() ? [\n 'id' => $report->getSavedSearch()->getUuid(),\n 'name' => $report->getSavedSearch()->getName(),\n ] : null,\n ],\n [\n 'id' => 'ask_jiminny_prompt',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Ask Jiminny prompt',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $prompts,\n 'value' => $report && $report->getAskAnythingPrompt() ? [\n 'id' => $report->getAskAnythingPrompt()->getUuid(),\n 'name' => $report->getAskAnythingPrompt()->getTitle(),\n ] : null,\n ],\n ],\n ];\n }\n\n private function updateResultNames(AutomatedReport $automatedReport): void\n {\n $results = $this->automatedReportsRepository->getResultsByReport($automatedReport);\n\n foreach ($results as $result) {\n $result->update(['name' => $this->getReportFileName($result)]);\n }\n }\n\n public function updateStatus(string $uuid, array $data): array\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $automatedReport->update([\n 'status' => $status,\n ]);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n private function generateOneOffReport(AutomatedReport $automatedReport): void\n {\n // the scheduler handles all the other frequency types\n if ($automatedReport->getStatus() === false || $automatedReport->getFrequency() !== self::FREQUENCY_ONE_OFF) {\n return;\n }\n\n $this->dispatcher->dispatch(new RequestGenerateReportJob($automatedReport->getUuid()));\n }\n\n public function getReport(string $uuid, ?Partner $partner = null): AutomatedReport\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n if ($partner !== null && ! $partner->isDefaultPartner() && $automatedReport->team->partner_id !== $partner->getId()) {\n throw new ModelNotFoundException('Report not found');\n }\n\n return $automatedReport;\n }\n\n public function get(string $uuid, ?Partner $partner = null): array\n {\n $automatedReport = $this->getReport($uuid, $partner);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function list(string $sortColumn = 'created_at', string $sortDirection = 'desc', ?Partner $partner = null): array\n {\n $results = [];\n $collection = $this->automatedReportsRepository->getAllStandardReports($sortColumn, $sortDirection, $partner);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function listAskJiminnyReports(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): array {\n $results = [];\n $collection = $this->automatedReportsRepository->getAskJiminnyReportsByUser($user, $sortColumn, $sortDirection);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function delete(string $uuid): void\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $automatedReport->delete();\n }\n\n public function createReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n return $this->automatedReportsRepository->createResult(\n array_merge(\n [\n 'report_id' => $automatedReport->getId(),\n 'status' => AutomatedReportResult::STATUS_DEFAULT,\n ],\n $data\n )\n );\n }\n\n public function getOrCreateReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n $existing = $this->automatedReportsRepository->findLatestSameDayDefaultOrFailedResult($automatedReport);\n\n if ($existing !== null) {\n $existing->update(['status' => AutomatedReportResult::STATUS_DEFAULT]);\n\n return $existing;\n }\n\n return $this->createReportResult($automatedReport, $data);\n }\n\n public function getReportResult(string $resultUuid): AutomatedReportResult\n {\n $report = $this->automatedReportsRepository->findResultByUuid($resultUuid);\n\n if (! $report) {\n throw new ModelNotFoundException('Report Result not found');\n }\n\n return $report;\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return $this->automatedReportsRepository->findChildResult($result, $type);\n }\n\n // prophet API calls\n /**\n * @throws ApplicationException\n */\n public function getGenerateReportPayload(AutomatedReport $automatedReport, string $reportResultUuid): array\n {\n $period = $this->calculateFromAndToDate($automatedReport);\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n return [\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResultUuid,\n 'report_type' => $automatedReport->getType(),\n 'media_types' => $automatedReport->getMediaTypes(),\n 'from_date' => $fromDate->startOfDay()->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->endOfDay()->format(DateTimeInterface::RFC3339),\n 'group_ids' => $automatedReport->getGroups(),\n 'call_deal_stage' => $automatedReport->getDealAtCallStages(),\n 'current_deal_stage' => $automatedReport->getCurrentDealStages(),\n 'deal_min_value' => $automatedReport->getDealValueMin(),\n 'deal_max_value' => $automatedReport->getDealValueMax(),\n 'call_types' => $automatedReport->getCallTypes(),\n 'call_duration_min_seconds' => $automatedReport->getCallDurationMin(),\n 'call_duration_max_seconds' => $automatedReport->getCallDurationMax(),\n 'special_requirements' => $automatedReport->getAdditionalPromptInput(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->formatReportPeriodName(\n $automatedReport->getFrequency(),\n $fromDate,\n $toDate,\n ),\n 'playbook_categories' => $automatedReport->getPlaybookCategories(),\n 'custom_name' => $automatedReport->getCustomName(),\n ];\n }\n\n // $inputPayload - FE payload structure\n public function getActivitiesCountPayload(array $inputPayload): array\n {\n // Use validateAndTransformData to validate and normalize input\n $validatedData = $this->validateAndTransformData($inputPayload);\n $period = $this->calculateFromAndToDatePeriod(\n $validatedData['frequency'],\n Carbon::parse($validatedData['from']),\n Carbon::parse($validatedData['to']),\n );\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n // Create payload similar to getGenerateReportPayload\n return [\n 'team_id' => $validatedData['team_id'],\n 'group_ids' => $validatedData['groups'] ?? [],\n 'report_type' => $validatedData['type'],\n 'from_date' => $fromDate->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->format(DateTimeInterface::RFC3339),\n 'call_deal_stage' => $validatedData['deal_at_call_stages'] ?? [],\n 'current_deal_stage' => $validatedData['current_deal_stages'] ?? [],\n 'deal_min_value' => $validatedData['deal_value_min'] ?? null,\n 'deal_max_value' => $validatedData['deal_value_max'] ?? null,\n 'call_types' => $validatedData['call_types'],\n 'call_duration_min_seconds' => $validatedData['call_duration_min'] ?? null,\n 'call_duration_max_seconds' => $validatedData['call_duration_max'] ?? null,\n 'special_requirements' => $validatedData['additional_prompt_input'] ?? null,\n 'playbook_categories' => $validatedData['playbook_categories'] ?? [],\n 'request_id' => null,\n 'callback_url' => null,\n ];\n }\n\n public function shouldSendReport(array $users, ?CarbonInterface $generatedAt = null): bool\n {\n if (empty($users)) {\n return false;\n }\n\n $earliestTz = collect($users)\n ->mapWithKeys(function (array $user) {\n $tz = new DateTimeZone($user['timezone']);\n $nowUtc = new DateTime('now', new DateTimeZone('UTC'));\n $offset = $tz->getOffset($nowUtc);\n\n return [$user['timezone'] => $offset];\n })\n ->sortDesc()\n ->keys()\n ->first();\n\n $now = Carbon::now($earliestTz);\n $isScheduledTime = (int) $now->format('H') === self::SENT_REPORT_AT_HOURS;\n\n if ($isScheduledTime) {\n return true;\n }\n\n return $this->hasPassedScheduledTime($generatedAt, $earliestTz);\n }\n\n public function hasPassedScheduledTime(?CarbonInterface $generatedAt, string $timezone): bool\n {\n if ($generatedAt === null) {\n return false;\n }\n\n $now = Carbon::now($timezone);\n $scheduledTime = $now->copy()->setTime(self::SENT_REPORT_AT_HOURS, 0, 0);\n\n if ($now->hour < self::SENT_REPORT_AT_HOURS) {\n $scheduledTime = $scheduledTime->subDay();\n }\n\n $scheduledTimeUtc = $scheduledTime->copy()->utc();\n $generatedAtUtc = $generatedAt->copy()->utc();\n $nowUtc = $now->copy()->utc();\n\n return $generatedAtUtc->lt($scheduledTimeUtc) && $nowUtc->gt($scheduledTimeUtc);\n }\n\n public function calculateFromAndToDatePeriod(\n string $frequency,\n ?Carbon $fromDate = null,\n ?Carbon $toDate = null,\n DateTimeZone|string|null $timezone = 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($timezone);\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()->subWeek()->startOfWeek(CarbonInterface::MONDAY),\n 'toDate' => $now->copy()->subWeek()->endOfWeek(CarbonInterface::SUNDAY),\n ],\n self::FREQUENCY_MONTHLY => [\n 'fromDate' => $now->copy()->subMonthNoOverflow()->startOfMonth(),\n 'toDate' => $now->copy()->subMonthNoOverflow()->endOfMonth(),\n ],\n self::FREQUENCY_QUARTERLY => [\n 'fromDate' => $now->copy()->subQuarterNoOverflow()->startOfQuarter(),\n 'toDate' => $now->copy()->subQuarterNoOverflow()->endOfQuarter(),\n ],\n default => throw new InvalidArgumentException(\"Unsupported frequency: {$frequency}\"),\n };\n }\n\n private function calculateFromAndToDate(AutomatedReport $automatedReport): array\n {\n return $this->calculateFromAndToDatePeriod(\n $automatedReport->getFrequency(),\n $automatedReport->getFrom(),\n $automatedReport->getTo()\n );\n }\n\n public function getAskJiminnyGenerateReportPayload(\n AutomatedReport $automatedReport,\n AutomatedReportResult $reportResult,\n array $activityIds,\n ): array {\n return [\n 'user_question' => $automatedReport->getAskAnythingPrompt()?->getContent(),\n 'call_ids' => array_map('strval', $activityIds),\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResult->getUuid(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->getReportPeriodName($reportResult),\n 'report_name' => $automatedReport->getCustomName(),\n ];\n }\n\n private function getCallbackUrl(): string\n {\n return $this->webhookService->route('jiminny.webhook.reports.ready');\n }\n\n /**\n * Validate and transform payload data for automated reports\n *\n * @param array $data\n *\n * @throws InvalidArgumentException\n *\n * @return array\n */\n private function validateAndTransformData(array $data): array\n {\n // Validate organization (team) and check feature\n $team = $this->validateOrganization($data['organization'] ?? null);\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $type = $this->validateReportType($data['report_type'] ?? null);\n $frequency = $this->validateFrequency($data['frequency'] ?? null);\n $additionalPromptInput = $this->validateAdditionalPromptInput(\n $data['additional_prompt_input'] ?? null\n );\n $customReportName = $this->validateCustomReportName($data['custom_name'] ?? null);\n\n // Prepare data for the database\n $reportData = [\n 'team_id' => $team->getId(),\n 'type' => $type,\n 'status' => $status,\n 'frequency' => $frequency,\n 'additional_prompt_input' => $additionalPromptInput,\n 'custom_name' => $customReportName,\n ];\n\n // Validate deal values\n $reportData = $this->validateDealValues($data, $reportData);\n\n // Validate date range\n $reportData = $this->validateDateRange($data, $reportData, $frequency);\n\n // Validate call durations\n $reportData = $this->validateCallDurations($data, $reportData);\n\n // Validate call types\n $reportData = $this->validateCallTypes($data, $reportData);\n\n // Validate media types\n $reportData = $this->validateMediaTypes($data, $reportData);\n\n // Validate groups\n if (isset($data['teams'])) {\n $reportData['groups'] = $this->validateAndGetGroupIds($team, $data['teams']);\n }\n\n // Validate deal stages\n $reportData = $this->validateDealStages($data, $reportData, $team, $type);\n\n // Validate playbook categories\n $reportData = $this->validatePlaybookCategories($data, $reportData, $team);\n\n // Validate recipients\n $reportData['recipients'] = [\n 'users' => $this->validateAndGetUserIdsByTeam($team, $data['recipients'] ?? []),\n ];\n\n if (isset($data['jiminny_recipients'])) {\n // Validate Jiminny recipients\n $reportData['jiminny_recipients'] = [\n 'users' => $this->validateAndGetJiminnyUserIds((array) $data['jiminny_recipients']),\n ];\n }\n\n return $reportData;\n }\n\n private function validateDealValues(array $data, array $reportData): array\n {\n if (isset($data['min_deal_value'])) {\n $reportData['deal_value_min'] = (int) $data['min_deal_value'];\n\n if ($reportData['deal_value_min'] > 4294967295 || $reportData['deal_value_min'] < 0) {\n throw new InvalidArgumentException('Min deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_deal_value'])) {\n $reportData['deal_value_max'] = (int) $data['max_deal_value'];\n\n if ($reportData['deal_value_max'] > 4294967295 || $reportData['deal_value_max'] < 0) {\n throw new InvalidArgumentException('Max deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['min_deal_value'], $data['max_deal_value'])\n && $data['min_deal_value'] > $data['max_deal_value']\n ) {\n throw new InvalidArgumentException('Min deal value cannot be greater than max deal value');\n }\n\n return $reportData;\n }\n\n private function validateDateRange(array $data, array $reportData, string $frequency): array\n {\n // Set date range only for one_off frequency\n if ($frequency === 'one_off') {\n if (isset($data['start_date_period'])) {\n $reportData['from'] = $this->parseDate($data['start_date_period']);\n }\n\n if (isset($data['end_date_period'])) {\n $reportData['to'] = $this->parseDate($data['end_date_period']);\n }\n\n if (empty($reportData['from']) || empty($reportData['to'])) {\n throw new InvalidArgumentException(\n 'Start date and end date are required for one_off frequency'\n );\n }\n } else {\n $reportData['from'] = null;\n $reportData['to'] = null;\n }\n\n return $reportData;\n }\n\n private function validateCallDurations(array $data, array $reportData): array\n {\n // Convert call durations from minutes to seconds\n if (isset($data['min_call_duration'])) {\n $reportData['call_duration_min'] = (int) $data['min_call_duration'] * 60;\n\n if ($reportData['call_duration_min'] > 4294967295 || $reportData['call_duration_min'] < 0) {\n throw new InvalidArgumentException('Min call duration should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_call_duration'])) {\n $reportData['call_duration_max'] = (int) $data['max_call_duration'] * 60;\n\n if ($reportData['call_duration_max'] > 4294967295 || $reportData['call_duration_max'] < 0) {\n throw new InvalidArgumentException('Max call duration should be between 0 and 4294967295');\n }\n }\n\n return $reportData;\n }\n\n private function validateCallTypes(array $data, array $reportData): array\n {\n // Set call types\n $reportData['call_types'] = $data['call_type'] ?? [];\n if (empty($reportData['call_types'])) {\n $reportData['call_types'] = self::getCallTypes();\n }\n\n foreach ($reportData['call_types'] as $callType) {\n if (! in_array($callType, self::getCallTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Call type %s is invalid', $callType));\n }\n }\n\n return $reportData;\n }\n\n private function validateMediaTypes(array $data, array $reportData): array\n {\n // Set media types from input data\n $reportData['media_types'] = $data['media_types'] ?? [];\n\n if (empty($reportData['media_types'])) {\n throw new InvalidArgumentException('Media types are required');\n }\n\n foreach ($reportData['media_types'] as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n throw new InvalidArgumentException(sprintf('Media type %s is invalid', $mediaType));\n }\n }\n\n return $reportData;\n }\n\n private function validateDealStages(array $data, array $reportData, Team $team, string $reportType): array\n {\n // Validate and set deal stages\n if (isset($data['deal_stage_at_call'])) {\n $reportData['deal_at_call_stages'] =\n $this->validateAndGetDealStageIds($team, $data['deal_stage_at_call'], 'Deal stage at call');\n }\n\n if (isset($data['current_deal_stage'])) {\n $reportData['current_deal_stages'] =\n $this->validateAndGetDealStageIds($team, $data['current_deal_stage'], 'Current deal stage');\n }\n\n // Ensure current_deal_stage is not provided for loss_analysis report type\n if ($reportType === self::TYPE_LOSS_ANALYSIS && ! empty($data['current_deal_stage'])) {\n throw new InvalidArgumentException('Current deal stage is not applicable for Loss Analysis reports');\n }\n\n return $reportData;\n }\n\n // transform uuid to id\n private function validatePlaybookCategories(array $data, array $reportData, Team $team): array\n {\n $key = 'playbook_categories';\n\n if (isset($data[$key])) {\n $payloadIds = $data[$key];\n $ids = [];\n\n foreach ($payloadIds as $uuid) {\n $uuid = (string) $uuid;\n\n try {\n $playbookCategory = $this->playbookCategoryRepository->findByUuid($uuid);\n } catch (Throwable $throwable) {\n Log::error(__METHOD__ . ' ' . $throwable->getMessage());\n\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory) {\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory->hasPlaybook()) {\n throw new InvalidArgumentException(sprintf('Playbook category %s has no playbook', $uuid));\n }\n\n if ($playbookCategory->getPlaybook()->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Playbook category %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $ids[] = $playbookCategory->getId();\n }\n\n $reportData[$key] = $ids;\n }\n\n return $reportData;\n }\n\n private function validateReportStatus($status): bool\n {\n if (! in_array($status, [true, false], true)) {\n throw new InvalidArgumentException('Report status is invalid');\n }\n\n return $status;\n }\n\n private function validateReportType($type): string\n {\n if (! in_array($type, self::getTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Report type is invalid: %s', $type));\n }\n\n return $type;\n }\n\n private function validateFrequency($frequency): string\n {\n if (! in_array($frequency, self::getFrequencies(), true)) {\n throw new InvalidArgumentException('Frequency is invalid');\n }\n\n return $frequency;\n }\n\n private function validateAdditionalPromptInput(?string $additionalPromptInput): ?string\n {\n if ($additionalPromptInput && strlen($additionalPromptInput) > 5000) {\n throw new InvalidArgumentException('Additional Prompt Input should be less than 5000 characters');\n }\n\n return $additionalPromptInput;\n }\n\n private function validateCustomReportName(?string $customReportName): ?string\n {\n if ($customReportName === null || $customReportName === '') {\n return null;\n }\n\n if (strlen($customReportName) > 70) {\n throw new InvalidArgumentException('Custom report name should be less than 70 characters');\n }\n\n return $customReportName;\n }\n\n private function validateOrganization(?string $organizationUuid): Team\n {\n if (! $organizationUuid) {\n throw new InvalidArgumentException('Organization is required');\n }\n\n $team = $this->teamRepository->idOrUuid($organizationUuid);\n\n if (! $team) {\n throw new InvalidArgumentException('Organization not found');\n }\n\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n throw new InvalidArgumentException('Organization does not have the Automated Reports feature');\n }\n\n return $team;\n }\n\n private function validateAndGetGroupIds(Team $team, array $teamUuids): array\n {\n $groupIds = [];\n\n foreach ($teamUuids as $uuid) {\n $group = $this->groupRepository->findByUuid($uuid);\n\n if ($group === null || $group->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Group %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $groupIds[] = $group->getId();\n\n }\n\n return $groupIds;\n }\n\n private function validateAndGetDealStageIds(Team $team, array $stageUuids, string $propertyLabel): array\n {\n $stageIds = [];\n\n foreach ($stageUuids as $uuid) {\n $stage = $this->stageRepository->findByUuid($uuid);\n\n if ($stage === null || $stage->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Stage %s not found for team %s for %s', $uuid, $team->getUuid(), $propertyLabel)\n );\n }\n\n $stageIds[] = $stage->getId();\n }\n\n return $stageIds;\n }\n\n private function validateAndGetUserIds(array $userUuids, callable $teamCheck): array\n {\n if (empty($userUuids)) {\n return [];\n }\n\n $userIds = [];\n\n foreach ($userUuids as $uuid) {\n $user = $this->userRepository->findByUuid($uuid);\n\n if (! $user || ! $user->isStatusActive()) {\n throw new InvalidArgumentException(\n sprintf('User %s not found or is not active', $uuid)\n );\n }\n\n if (! $teamCheck($user)) {\n throw new InvalidArgumentException(\n sprintf('User %s does not belong to the allowed team(s)', $uuid)\n );\n }\n\n $userIds[] = $user->getId();\n }\n\n return $userIds;\n }\n\n private function validateAndGetUserIdsByTeam(Team $team, array $userUuids): array\n {\n return $this->validateAndGetUserIds($userUuids, fn ($user) => $user->getTeamId() === $team->getId());\n }\n\n private function validateAndGetJiminnyUserIds(array $userUuids): array\n {\n $allowedTeamIds = config('kiosk.teamIds', []);\n\n return $this->validateAndGetUserIds($userUuids, fn ($user) => in_array($user->getTeamId(), $allowedTeamIds, true));\n }\n\n private function parseDate(string $dateString): string\n {\n return date('Y-m-d H:i:s', strtotime($dateString));\n }\n\n private function generateReportResultViewUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.view', ['uuid' => $result->getUuid()]);\n }\n\n private function generateReportResultDownloadUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.download', ['uuid' => $result->getUuid()]);\n }\n\n private function getReportMediaRouteResource(AutomatedReportResult $result): string\n {\n if ($result->getMediaType() === self::MEDIA_TYPE_PDF) {\n return self::PDF_KEY;\n } elseif ($result->getMediaType() === self::MEDIA_TYPE_PODCAST) {\n return self::AUDIO_KEY;\n }\n\n throw new \\InvalidArgumentException('Unknown media type.');\n }\n\n public function getMediaPath(AutomatedReportResult $result): ?string\n {\n $url = match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => $result->getPdfUrl(),\n self::MEDIA_TYPE_PODCAST => $result->getPodcastAudioUrl(),\n default => null,\n };\n\n if ($url === null) {\n return null;\n }\n\n $path = parse_url(trim($url, '\"\\''), PHP_URL_PATH);\n\n return $path ?: null;\n }\n\n public function getFilenameSuffix(AutomatedReportResult $result): ?string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => 'Podcast',\n default => null,\n };\n }\n\n public function getMailSubjectSuffix(AutomatedReportResult $result): string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => 'report',\n self::MEDIA_TYPE_PODCAST => 'podcast',\n default => '',\n };\n }\n\n public function getMediaTypeMetadata(AutomatedReportResult $result): array\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => ['extension' => 'mp3', 'mime' => 'audio/mpeg'],\n self::MEDIA_TYPE_PDF => ['extension' => 'pdf', 'mime' => 'application/pdf'],\n default => ['extension' => null, 'mime' => null],\n };\n }\n\n public function deleteS3Files(AutomatedReportResult $result): void\n {\n $teamUuid = $result->getReport()->getTeam()->getUuid();\n $reportUuid = $result->getUuid();\n\n // delete all files for a report uuid no mather of pdf, podcast, or both\n // in case of both - the podcast files are linked to the pdf (parent) uuid\n // pdf and podcast date times should be close\n $path = sprintf('%s/%s/%s', $teamUuid, self::S3_DIR, $reportUuid);\n\n foreach (self::FILE_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted S3 file', [\n 'path' => $file,\n ]);\n }\n }\n\n foreach (self::FILE_PODCAST_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '_podcast.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted Podcast S3 file', [\n 'path' => $file,\n ]);\n }\n }\n }\n\n /**\n *\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): Collection\n {\n return $this->automatedReportsRepository->getTeamIdsWithReportsResults($teamId);\n }\n\n /**\n * Core delete logic for report results using a query\n *\n * @param Builder $query\n * @param array $logContext\n *\n * @return int\n */\n private function deleteReportResultsByQuery(Builder $query, array $logContext = []): int\n {\n $deletedCount = 0;\n\n if ($query->exists()) {\n Log::info(\n 'Run delete report results',\n array_merge(\n $logContext,\n [\n 'service' => 'AutomatedReportsService',\n ]\n )\n );\n\n $query->chunkById(50, function ($results) use (&$deletedCount, $logContext) {\n foreach ($results as $result) {\n $this->deleteReportResult($result);\n $deletedCount++;\n\n Log::info(\n 'Deleted a report result',\n array_merge(\n $logContext,\n [\n 'result_id' => $result->getId(),\n 'report_id' => $result->getReportId(),\n ]\n )\n );\n }\n });\n }\n\n return $deletedCount;\n }\n\n /**\n * Delete report results for a team by retention period\n *\n * @param Team $team\n * @param CarbonImmutable $retentionDate\n *\n * @return int Number of deleted report results\n */\n public function deleteReportsResultsInRetentionPeriod(Team $team, CarbonImmutable $retentionDate): int\n {\n $reportIds = $this->automatedReportsRepository->getReportIdsByTeam($team);\n\n if ($reportIds->isEmpty()) {\n return 0;\n }\n\n $query = $this->automatedReportsRepository\n ->getReportResultsQueryForRetention($team, $retentionDate);\n\n return $this->deleteReportResultsByQuery($query, [\n 'team_id' => $team->getId(),\n 'retention_date' => $retentionDate->toDateTimeString(),\n ]);\n }\n\n /**\n * Delete ALL report results for a specific automated report\n *\n * @param string $uuid\n *\n * @return int\n */\n public function deleteReportResults(string $uuid): int\n {\n $report = $this->getReport($uuid);\n\n $query = $this->automatedReportsRepository->getResultsByReportQuery($report);\n\n return $this->deleteReportResultsByQuery($query, [\n 'report_uuid' => $uuid,\n 'report_id' => $report->getId(),\n ]);\n }\n\n public function deleteReportResult(AutomatedReportResult $result): void\n {\n $this->deleteS3Files($result);\n\n $result->delete();\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getTeamReports(Team $team): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getReportsByTeam($team);\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getReportResults(AutomatedReport $report): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getResultsByReport($report);\n }\n\n public function deleteAllReportResults(AutomatedReport $report): void\n {\n $results = $this->getReportResults($report);\n\n /** @var AutomatedReportResult $result */\n foreach ($results as $result) {\n Log::info('Deleting result', [\n 'report' => $report->getId(),\n 'result' => $result->getId(),\n ]);\n\n $this->deleteReportResult($result);\n }\n }\n\n public function deleteAllData(Team $team): void\n {\n Log::info('Deleting automated report and results for team', [\n 'team' => $team->getId(),\n ]);\n\n $reports = $this->getTeamReports($team);\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n Log::info('Deleting report', [\n 'team' => $team->getId(),\n 'report' => $report->getId(),\n ]);\n\n $this->deleteAllReportResults($report);\n\n $report->delete();\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Carbon\\CarbonImmutable;\nuse Carbon\\CarbonInterface;\nuse Carbon\\Exceptions\\InvalidFormatException;\nuse DateTime;\nuse DateTimeInterface;\nuse DateTimeZone;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\InputTypeEnum;\nuse Jiminny\\Component\\AskAnything\\AskAnythingPromptService;\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Contracts\\Repositories\\PlaybookCategoryRepository;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\ModelNotFoundException;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Partner;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Repositories\\GroupRepository;\nuse Jiminny\\Repositories\\SearchRepository;\nuse Jiminny\\Repositories\\StageRepository;\nuse Throwable;\n\nclass AutomatedReportsService\n{\n public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';\n public const string TYPE_ASK_JIMINNY = 'ask_jiminny';\n\n /**\n * Standard report types (used by kiosk for existing automated reports).\n */\n // @TODO this will add filter, however if we need to control feature by FF we need conditional logic\n public const array TYPES = [\n ['id' => 'exec_summary', 'name' => 'Exec Summary'],\n ['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],\n ['id' => 'product_feedback', 'name' => 'Product Feedback'],\n ['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],\n// ['id' => 'questions', 'name' => 'Questions'],\n// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],\n ];\n\n public const array ALL_TYPES = [\n ...self::TYPES,\n ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],\n ];\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\n /**\n * Frequencies for standard (non-Ask Jiminny) reports.\n */\n public const array FREQUENCIES = [\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n\n /**\n * Frequencies for Ask Jiminny reports.\n */\n public const array ASK_JIMINNY_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ];\n\n public const string MEDIA_TYPE_PDF = 'pdf';\n public const string MEDIA_TYPE_PODCAST = 'podcast';\n public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];\n public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];\n public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];\n public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];\n\n public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];\n public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];\n public const int SENT_REPORT_AT_HOURS = 5;\n public const string PDF_KEY = 'pdf';\n public const string AUDIO_KEY = 'audio';\n\n private const array ALL_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n private const string S3_DIR = 'reports';\n private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];\n private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];\n\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly GroupRepository $groupRepository,\n private readonly UserRepository $userRepository,\n private readonly StageRepository $stageRepository,\n private readonly DealStagesService $dealStagesService,\n private readonly RecipientsService $recipientsService,\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly Webhook $webhookService,\n private readonly BusDispatcher $dispatcher,\n private readonly ActivityTypeService $activityTypeService,\n private readonly PlaybookCategoryRepository $playbookCategoryRepository,\n private readonly AskAnythingPromptService $askAnythingPromptService,\n private readonly SearchRepository $activitySearchRepository,\n private readonly AskAnythingRepository $askAnythingRepository,\n ) {\n }\n\n public static function getTypes(): array\n {\n $types = self::TYPES;\n\n return array_map(static function ($type) {\n return $type['id'];\n }, $types);\n }\n\n public static function getCallTypes(): array\n {\n return array_map(static function ($callType) {\n return $callType['id'];\n }, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);\n }\n\n public static function getFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::FREQUENCIES);\n }\n\n // front-facing structure\n public function getReportEnabledFieldData(bool $value = false): array\n {\n return [\n 'id' => 'report_enabled',\n 'label' => '',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'value' => $value,\n ];\n }\n\n // Organizations = Teams\n public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array\n {\n $options = $this->getTeams(partner: $partner);\n\n if ($shortVersion) {\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value,\n 'dependencies' => [\n 'teams',\n 'deal_stage_at_call',\n 'current_deal_stage',\n 'recipients',\n ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,\n ],\n 'dependsOn' => [],\n ];\n }\n\n // Teams = Groups\n public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array\n {\n if ($shortVersion) {\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value, // value should be an array of objects {id, name}\n 'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],\n 'dependsOn' => [],\n ];\n }\n\n public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array\n {\n $types = [];\n if ($team instanceof Team) {\n if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n $types = self::TYPES;\n }\n if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {\n $types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];\n }\n } else {\n $types = self::TYPES;\n }\n\n if ($shortVersion) {\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'options' => $types,\n ];\n }\n\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $types,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getFrequencyFieldData(?string $value = null): array\n {\n return [\n 'id' => 'frequency',\n 'label' => 'Frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::FREQUENCIES,\n 'value' => $value,\n 'dependencies' => ['period'],\n 'dependsOn' => [],\n ];\n }\n\n public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array\n {\n return [\n 'id' => 'period',\n 'label' => 'Select one-off period',\n 'inputType' => InputTypeEnum::DATE_RANGE,\n 'required' => true,\n 'placeholder' => 'Select',\n 'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],\n 'queryParams' => [\n 'startDate' => 'start_date_period',\n 'endDate' => 'end_date_period',\n ],\n 'dependencies' => [],\n 'dependsOn' => ['frequency'],\n ];\n }\n\n public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array\n {\n return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);\n }\n\n public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);\n }\n\n public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);\n }\n\n public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'deal_value',\n 'label' => 'Deal Value',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_deal_value',\n 'max' => 'max_deal_value',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array\n {\n $value = [];\n $conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;\n $dialerOn && $value[] = self::CALL_TYPE_DIALER;\n\n return [\n 'id' => 'call_type',\n 'label' => 'Call Type',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => [\n self::CALL_TYPE_CONFERENCE,\n self::CALL_TYPE_DIALER,\n ],\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getMediaTypeFieldData(?AutomatedReport $report = null): array\n {\n $value = [];\n\n if ($report) {\n $value = $this->transformMediaTypes($report);\n }\n\n return [\n 'id' => 'media_types',\n 'label' => 'Export as',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => self::MEDIA_TYPE_OBJECTS,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'call_duration',\n 'label' => 'Call Duration',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_call_duration',\n 'max' => 'max_call_duration',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getRecipientsFieldData(?Team $team = null, array $value = []): array\n {\n return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);\n }\n\n public function getJiminnyRecipientsFieldData(array $value = []): array\n {\n return $this->recipientsService->getJiminnyRecipientsFieldData($value);\n }\n\n public function getAdditionalPromptInputFieldData(?string $value = null): array\n {\n return [\n 'id' => 'additional_prompt_input',\n 'label' => 'Special requirements',\n 'inputType' => InputTypeEnum::TEXTAREA,\n 'required' => false,\n 'placeholder' => 'What should be the focus of the report?',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCustomReportNameFieldData(?string $value = null): array\n {\n return [\n 'id' => 'custom_name',\n 'label' => 'Custom report name',\n 'inputType' => InputTypeEnum::TEXT,\n 'required' => false,\n 'placeholder' => 'Enter custom name',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n // data providers\n public function getTeams(?Partner $partner = null): array\n {\n $teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);\n\n $teamData = [];\n foreach ($teams as $team) {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n continue;\n }\n\n $teamData[] = $this->transformTeam($team);\n }\n\n return $teamData;\n }\n\n public function getTeamGroups(string $teamUuid): array\n {\n $data = [];\n $team = $this->getTeam($teamUuid);\n\n if ($team !== null) {\n $groups = $team->groups()->get();\n\n foreach ($groups as $group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array\n {\n $data = [];\n $teams = $this->getTeams(partner: $partner);\n\n foreach ($teams as $team) {\n if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {\n continue;\n }\n\n $data[] = [\n 'label' => $team['name'],\n 'groups' => $this->getTeamGroups($team['id']),\n ];\n }\n\n return $data;\n }\n\n public function getTeam(string $teamUuid): ?Team\n {\n return $this->teamRepository->idOrUuid($teamUuid);\n }\n\n public function getTeamById(int $teamId): ?Team\n {\n return $this->teamRepository->find($teamId);\n }\n\n public function getGroupsUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportGroups = $report->getGroups();\n foreach ($reportGroups as $groupId) {\n if ($group = $this->groupRepository->find($groupId)) {\n $uuids[] = $group->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getPlaybookCategoriesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $playbookCategories = $report->getPlaybookCategories();\n foreach ($playbookCategories as $id) {\n if ($category = $this->playbookCategoryRepository->find($id)) {\n $uuids[] = $category->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getDealAtCallStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getDealAtCallStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getCurrentDealStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getCurrentDealStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getRecipients());\n }\n\n public function getJiminnyUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function extractUserUuids(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => $user->getUuid())\n ->values()\n ->all();\n }\n\n // get mail data\n public function getRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getRecipients());\n }\n\n /**\n * @return array<UserContract>\n */\n public function getRecipientUserObjects(AutomatedReport $report): array\n {\n $userIds = $report->getRecipients()['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->values()\n ->all();\n }\n\n private function getJiminnyRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function buildRecipientUsers(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => [\n 'email' => $user->getEmailAddress(),\n 'name' => $user->getName(),\n 'timezone' => $user->getTimezone()->getName(),\n ])\n ->values()\n ->all();\n }\n\n public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array\n {\n if ($report->isAskJiminnyReport()) {\n $recipients = $this->resolveAskJiminnyRecipients($report);\n } else {\n $recipients = $this->getRecipientUsers($report);\n if ($includeJiminny) {\n $recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));\n }\n }\n\n $emails = [];\n\n return array_values(array_filter(\n $recipients,\n static function ($recipient) use (&$emails) {\n if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {\n return false;\n }\n $emails[] = $recipient['email'];\n\n return true;\n }\n ));\n }\n\n private function resolveAskJiminnyRecipients(AutomatedReport $report): array\n {\n $recipients = [];\n\n $creator = $report->getCreator();\n if ($creator !== null) {\n $recipients[] = [\n 'email' => $creator->getEmailAddress(),\n 'name' => $creator->getName(),\n 'timezone' => $creator->getTimezone()->getName(),\n ];\n }\n\n return array_merge(\n $recipients,\n $this->buildRecipientUsers($report->getRecipients()),\n $this->getGroupRecipientUsers($report),\n );\n }\n\n private function getGroupRecipientUsers(AutomatedReport $report): array\n {\n $users = [];\n foreach ($report->getGroups() as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group === null) {\n continue;\n }\n foreach ($group->getMembers() as $member) {\n $users[] = [\n 'email' => $member->getEmailAddress(),\n 'name' => $member->getName(),\n 'timezone' => $member->getTimezone()->getName(),\n ];\n }\n }\n\n return $users;\n }\n\n public function getReportTypeName(AutomatedReportResult $report): string\n {\n $type = $report->getReport()->getType();\n\n $getType = $this->transformReportType($type);\n\n return $getType['name'];\n }\n\n public function getReportPeriodName(AutomatedReportResult $report): string\n {\n $from = $report->getFromDate();\n $to = $report->getToDate();\n $frequency = $report->getReport()->getFrequency();\n\n if ($from === null || $to === null) {\n if (! $report->getReport()->isAskJiminnyReport()) {\n $invalidPeriod = $from === null ? 'from' : 'to';\n\n throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);\n }\n\n $timezone = $report->getReport()->getCreator()?->getTimezone();\n $period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);\n $from = $period['fromDate'];\n $to = $period['toDate'];\n }\n\n return $this->formatReportPeriodName($frequency, $from, $to);\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 getReportTeamsName(AutomatedReportResult $report): string\n {\n $groups = $report->getGroups();\n\n if (empty($groups)) {\n return 'All';\n }\n\n // Get group names from repository\n $groupNames = [];\n foreach ($groups as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group) {\n $groupNames[] = $group->getName();\n }\n }\n\n if (count($groupNames) === 1) {\n // Single team format\n $teamsName = $groupNames[0];\n } else {\n // Multiple teams format\n $teamsName = implode(', ', $groupNames);\n }\n\n return $teamsName;\n }\n\n public function getReportFileName(AutomatedReportResult $report): string\n {\n $customName = $report->getReport()->getCustomName();\n $periodName = $this->getReportPeriodName($report);\n $filenameSuffix = $this->getFilenameSuffix($report);\n\n if ($customName) {\n if ($filenameSuffix) {\n $customName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$customName} - {$periodName}\");\n }\n\n $baseName = $this->getReportTypeName($report);\n\n if ($filenameSuffix) {\n $baseName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}\");\n }\n\n public function getReportFileNameWithExtension(AutomatedReportResult $result): string\n {\n $extension = $this->getMediaTypeMetadata($result)['extension'];\n\n return $this->getReportFileName($result) . '.' . $extension;\n }\n\n public function sanitizeFileName(string $fileName): string\n {\n return str_replace(['/', '\\\\'], '-', $fileName);\n }\n\n public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool\n {\n $recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);\n if (in_array($user->getId(), $recipientIds, true)) {\n return true;\n }\n\n if ($report->isAskJiminnyReport()) {\n $groupId = $user->getGroupId();\n if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {\n return true;\n }\n }\n\n return false;\n }\n\n public function transformReportResults(Collection $automatedReportResults): array\n {\n $data = [];\n foreach ($automatedReportResults as $automatedReportResult) {\n /** @var AutomatedReportResult $automatedReportResult */\n\n $report = $automatedReportResult->getReport();\n\n $createdBy = $report->getCreator();\n $creator = [\n 'id' => $createdBy?->getUuid(),\n 'name' => $createdBy?->getName(),\n 'email' => $createdBy?->getEmailAddress(),\n 'photoUrl' => $createdBy?->getPhotoUrl(),\n ];\n\n $data[] = [\n 'id' => $automatedReportResult->getUuid(),\n 'name' => $automatedReportResult->getName(),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n 'recipients' => $this->buildRecipients($report),\n 'report_type' => $this->transformReportType($report->getType()),\n 'media_type' => $automatedReportResult->getMediaType(),\n 'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),\n 'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),\n 'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),\n 'creator' => $creator,\n ];\n }\n\n return $data;\n }\n\n private function buildRecipients(AutomatedReport $report): array\n {\n $creatorUuid = $report->getCreator()?->getUuid();\n\n $recipients = array_values(array_filter(\n $this->transformRecipients($report->getRecipients()),\n static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,\n ));\n\n if (! $report->isAskJiminnyReport()) {\n return $recipients;\n }\n\n return [\n ...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),\n ...$recipients,\n ];\n }\n\n public function hasCallTypeConference(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);\n }\n\n public function hasCallTypeDialer(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);\n }\n\n // transformers\n private function transformTeam(Team $team): array\n {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n return [];\n }\n\n return [\n 'id' => $team->getUuid(),\n 'name' => $team->getName(),\n ];\n }\n\n private function transformReportFullView(AutomatedReport $report): array\n {\n $base = $this->transformReportBase($report);\n\n return $report->getType() === self::TYPE_ASK_JIMINNY\n ? $base + $this->transformAskJiminnyFields($report)\n : $base + $this->transformStandardReportFields($report);\n }\n\n private function transformReportBase(AutomatedReport $report): array\n {\n return [\n 'id' => $report->getUuid(),\n 'organization' => $this->transformOrganization(team: $report->getTeam()),\n 'report_type' => $this->transformReportType($report->getType()),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n ];\n }\n\n private function transformStandardReportFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n\n return [\n 'report_enabled' => $report->getStatus(),\n 'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),\n 'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),\n 'deal_value_min' => $report->getDealValueMin(),\n 'deal_value_max' => $report->getDealValueMax(),\n 'call_types' => $this->transformCallType($report->getCallTypes()),\n 'media_types' => $this->transformMediaTypes($report),\n 'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),\n 'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),\n 'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),\n 'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),\n 'recipients' => $this->transformRecipients($report->getRecipients()),\n 'created_by' => $this->transformCreator($report->getCreator()),\n 'additional_prompt_input' => $report->getAdditionalPromptInput(),\n 'custom_name' => $report->getCustomName(),\n 'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),\n 'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),\n 'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),\n ];\n }\n\n private function transformAskJiminnyFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n $creatorId = $report->getAttribute('created_by');\n $explicitUserIds = array_values(array_filter(\n $report->getRecipients()['users'] ?? [],\n static fn ($id) => $id !== $creatorId\n ));\n\n return [\n 'report_name' => $report->getCustomName(),\n 'enabled' => $report->getStatus(),\n 'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),\n 'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),\n 'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),\n 'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),\n ];\n }\n\n private function transformOrganization(?Team $team): array\n {\n return [\n 'id' => $team?->getUuid(),\n 'name' => $team?->getName(),\n ];\n }\n\n private function transformReportType(string $type): array\n {\n foreach (self::ALL_TYPES as $typeItem) {\n if ($typeItem['id'] === $type) {\n return $typeItem;\n }\n }\n\n return [];\n }\n\n private function transformCallType(array $types): array\n {\n $result = [];\n $callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];\n\n foreach ($types as $type) {\n foreach ($callTypes as $callTypeItem) {\n if ($callTypeItem['id'] === $type) {\n $result[] = $callTypeItem;\n\n break;\n }\n }\n }\n\n return $result;\n }\n\n private function transformMediaTypes(AutomatedReport $report): array\n {\n $values = [];\n\n foreach ($report->getMediaTypes() as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n continue;\n }\n\n $values[] = match ($mediaType) {\n self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,\n self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,\n };\n }\n\n return $values;\n }\n\n private function transformFrequency(string $frequency): array\n {\n foreach (self::ALL_FREQUENCIES as $frequencyItem) {\n if ($frequencyItem['id'] === $frequency) {\n return $frequencyItem;\n }\n }\n\n return [];\n }\n\n public function transformDurationToMinutes(?int $duration): ?int\n {\n if (! $duration) {\n return null;\n }\n\n return (int) ($duration / 60);\n }\n\n private function transformGroups(?Team $team, array $groupsIds): array\n {\n if (empty($groupsIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($groupsIds as $groupId) {\n $group = $team->groups()->where('id', $groupId)->first();\n\n if ($group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n 'photoUrl' => $group->getPhotoUrl(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformStages(?Team $team, array $stagesIds): array\n {\n if (empty($stagesIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($stagesIds as $stageId) {\n $stage = $team->stages()->where('id', $stageId)->first();\n\n if ($stage) {\n $data[] = [\n 'id' => $stage->getUuid(),\n 'name' => $stage->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformRecipients(array $recipients): array\n {\n $users = [];\n foreach ($recipients['users'] ?? [] as $userId) {\n $users[] = $this->transformUser($userId);\n }\n\n return $users;\n }\n\n private function transformCreator(?User $user): ?array\n {\n if ($user === null) {\n return null;\n }\n\n return $this->transformUser($user->getId());\n }\n\n private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array\n {\n if ($prompt === null) {\n return null;\n }\n\n return [\n 'id' => $prompt->getUuid(),\n 'name' => $prompt->getTitle(),\n ];\n }\n\n private function transformSafeSearch(?Search $search): ?array\n {\n if ($search === null) {\n return null;\n }\n\n return [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ];\n }\n\n private function transformUser(int $userId): array\n {\n /* @var ?User $user */\n $user = $this->userRepository->find($userId);\n\n return [\n 'id' => $user?->getUuid(),\n 'name' => $user?->getName(),\n 'email' => $user?->getEmailAddress(),\n 'photoUrl' => $user?->getPhotoUrl(),\n ];\n }\n\n public function create(array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $validatedData['created_by'] = auth()->id();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function update(string $uuid, array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $report = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $report) {\n throw new InvalidArgumentException('Report not found');\n }\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Create an Ask Jiminny report.\n */\n public function createAskJiminnyReport(array $data, User $creator): array\n {\n $validatedData = $this->validateAskJiminnyReportData($data, $creator);\n $validatedData['created_by'] = $creator->getId();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Update an Ask Jiminny report.\n */\n public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array\n {\n if (! $report->isAskJiminnyReport()) {\n throw new InvalidArgumentException('Report is not an Ask Jiminny report');\n }\n\n $validatedData = $this->validateAskJiminnyReportData($data, $user);\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array\n {\n if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {\n throw new InvalidArgumentException(\n 'This report is missing a saved search or prompt. ' .\n 'Edit the report to complete the setup before enabling it.'\n );\n }\n\n $this->automatedReportsRepository->update($report, ['status' => $status]);\n\n return $this->transformReportFullView($report->fresh());\n }\n\n /**\n * Validate and transform data for Ask Jiminny reports.\n */\n private function validateAskJiminnyReportData(array $data, User $user): array\n {\n // Validate name\n $name = trim($data['report_name'] ?? '');\n if (empty($name)) {\n throw new InvalidArgumentException('Report name is required');\n }\n if (mb_strlen($name) > 50) {\n throw new InvalidArgumentException('Report name must be 50 characters or less');\n }\n\n // Validate frequency (only daily, weekly, monthly for Ask Jiminny)\n $frequency = $data['frequency'] ?? null;\n $askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];\n if (! in_array($frequency, $askJiminnyFrequencies, true)) {\n throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');\n }\n\n // Validate expiration date\n $expiresAt = $data['expires_on'] ?? null;\n if (empty($expiresAt)) {\n throw new InvalidArgumentException('Expiration date is required');\n }\n\n try {\n $expiresAtDate = Carbon::parse($expiresAt);\n } catch (InvalidFormatException $e) {\n throw new InvalidArgumentException('Expiration date format is invalid');\n }\n $maxExpiration = Carbon::now()->addYear()->endOfDay();\n if ($expiresAtDate->gt($maxExpiration)) {\n throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');\n }\n if ($expiresAtDate->isPast()) {\n throw new InvalidArgumentException('Expiration date cannot be in the past');\n }\n\n // Validate saved search\n $activitySearchId = $data['saved_search'] ?? null;\n if (empty($activitySearchId)) {\n throw new InvalidArgumentException('Saved search is required');\n }\n $savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);\n if (! $savedSearch) {\n throw new InvalidArgumentException('Saved search not found or does not belong to you');\n }\n\n // Validate saved prompt\n $askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;\n if (empty($askAnythingPromptId)) {\n throw new InvalidArgumentException('Ask Jiminny prompt is required');\n }\n $prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);\n if (! $prompt) {\n throw new InvalidArgumentException('Ask Jiminny prompt not found');\n }\n\n // Validate status\n $status = $data['enabled'] ?? false;\n\n $recipientUserIds = [$user->getId()];\n\n if (! empty($data['share_users'])) {\n $sharedUserIds = $this->validateAndGetUserIdsByTeam(\n $user->team,\n (array) $data['share_users']\n );\n $recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);\n }\n\n $sharedGroupIds = [];\n if (! empty($data['share_teams'])) {\n $sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);\n }\n\n $recipientUserIds = array_values(array_unique($recipientUserIds));\n\n return [\n 'team_id' => $user->getTeamId(),\n 'type' => self::TYPE_ASK_JIMINNY,\n 'status' => (bool) $status,\n 'frequency' => $frequency,\n 'custom_name' => $name,\n 'activity_search_id' => $savedSearch->getId(),\n 'ask_anything_prompt_id' => $prompt->getId(),\n 'expires_at' => $expiresAtDate->toDateString(),\n 'media_types' => [self::MEDIA_TYPE_PDF],\n 'call_types' => [],\n 'recipients' => ['users' => $recipientUserIds],\n 'groups' => $sharedGroupIds,\n ];\n }\n\n public static function getAskJiminnyFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::ASK_JIMINNY_FREQUENCIES);\n }\n\n public function getAskJiminnyReportFilters(User $user): array\n {\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n return [\n [\n 'id' => 'prompt',\n 'label' => 'Prompt',\n 'options' => $prompts,\n ],\n [\n 'id' => 'saved_search',\n 'label' => 'Saved Search',\n 'options' => $savedSearches,\n ],\n ];\n }\n\n public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array\n {\n $team = $user->getTeam();\n $userTimezone = $user->getTimezone();\n\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n $teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ])->values()->all();\n\n $shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];\n\n $sharedTeamsValue = [];\n $sharedUsersValue = [];\n if ($report) {\n $sharedTeamsValue = $this->transformGroups($team, $report->getGroups());\n\n $recipientUserIds = $report->getRecipients()['users'] ?? [];\n $creatorId = $report->getAttribute('created_by');\n $sharedUserIds = array_values(array_filter(\n $recipientUserIds,\n static fn ($id) => $id !== $creatorId\n ));\n $sharedUsersValue = collect($sharedUserIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (User $u) => [\n 'id' => $u->getUuid(),\n 'name' => $u->getName(),\n ])\n ->values()\n ->all();\n }\n\n return [\n 'fields' => [\n [\n 'id' => 'enabled',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'label' => '',\n 'value' => $report?->getStatus() ?? false,\n ],\n [\n 'id' => 'report_name',\n 'inputType' => InputTypeEnum::TEXT,\n 'label' => 'Name',\n 'placeholder' => 'Enter name',\n 'required' => true,\n 'validation' => ['maxLength' => 50],\n 'value' => $report?->getCustomName() ?? '',\n ],\n [\n 'id' => 'frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Frequency',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::ASK_JIMINNY_FREQUENCIES,\n 'value' => $report ? $this->transformFrequency($report->getFrequency()) : null,\n ],\n [\n 'id' => 'expires_on',\n 'inputType' => InputTypeEnum::DATE,\n 'label' => 'Expires on',\n 'required' => true,\n 'placeholder' => 'Select',\n 'validation' => [\n 'minDate' => now($userTimezone)->toDateString(),\n 'maxDate' => now($userTimezone)->addYear()->toDateString(),\n ],\n 'value' => $report?->getExpiresAt()?->toDateString(),\n ],\n [\n 'id' => 'share_teams',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team',\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $teamGroups,\n 'value' => $sharedTeamsValue,\n ],\n [\n 'id' => 'share_users',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team member',\n 'required' => false,\n 'placeholder' => 'Select',\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'options' => $shareUsers,\n 'value' => $sharedUsersValue,\n ],\n [\n 'id' => 'saved_search',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Saved search',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $savedSearches,\n 'value' => $report && $report->getSavedSearch() ? [\n 'id' => $report->getSavedSearch()->getUuid(),\n 'name' => $report->getSavedSearch()->getName(),\n ] : null,\n ],\n [\n 'id' => 'ask_jiminny_prompt',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Ask Jiminny prompt',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $prompts,\n 'value' => $report && $report->getAskAnythingPrompt() ? [\n 'id' => $report->getAskAnythingPrompt()->getUuid(),\n 'name' => $report->getAskAnythingPrompt()->getTitle(),\n ] : null,\n ],\n ],\n ];\n }\n\n private function updateResultNames(AutomatedReport $automatedReport): void\n {\n $results = $this->automatedReportsRepository->getResultsByReport($automatedReport);\n\n foreach ($results as $result) {\n $result->update(['name' => $this->getReportFileName($result)]);\n }\n }\n\n public function updateStatus(string $uuid, array $data): array\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $automatedReport->update([\n 'status' => $status,\n ]);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n private function generateOneOffReport(AutomatedReport $automatedReport): void\n {\n // the scheduler handles all the other frequency types\n if ($automatedReport->getStatus() === false || $automatedReport->getFrequency() !== self::FREQUENCY_ONE_OFF) {\n return;\n }\n\n $this->dispatcher->dispatch(new RequestGenerateReportJob($automatedReport->getUuid()));\n }\n\n public function getReport(string $uuid, ?Partner $partner = null): AutomatedReport\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n if ($partner !== null && ! $partner->isDefaultPartner() && $automatedReport->team->partner_id !== $partner->getId()) {\n throw new ModelNotFoundException('Report not found');\n }\n\n return $automatedReport;\n }\n\n public function get(string $uuid, ?Partner $partner = null): array\n {\n $automatedReport = $this->getReport($uuid, $partner);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function list(string $sortColumn = 'created_at', string $sortDirection = 'desc', ?Partner $partner = null): array\n {\n $results = [];\n $collection = $this->automatedReportsRepository->getAllStandardReports($sortColumn, $sortDirection, $partner);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function listAskJiminnyReports(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): array {\n $results = [];\n $collection = $this->automatedReportsRepository->getAskJiminnyReportsByUser($user, $sortColumn, $sortDirection);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function delete(string $uuid): void\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $automatedReport->delete();\n }\n\n public function createReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n return $this->automatedReportsRepository->createResult(\n array_merge(\n [\n 'report_id' => $automatedReport->getId(),\n 'status' => AutomatedReportResult::STATUS_DEFAULT,\n ],\n $data\n )\n );\n }\n\n public function getOrCreateReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n $existing = $this->automatedReportsRepository->findLatestSameDayDefaultOrFailedResult($automatedReport);\n\n if ($existing !== null) {\n $existing->update(['status' => AutomatedReportResult::STATUS_DEFAULT]);\n\n return $existing;\n }\n\n return $this->createReportResult($automatedReport, $data);\n }\n\n public function getReportResult(string $resultUuid): AutomatedReportResult\n {\n $report = $this->automatedReportsRepository->findResultByUuid($resultUuid);\n\n if (! $report) {\n throw new ModelNotFoundException('Report Result not found');\n }\n\n return $report;\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return $this->automatedReportsRepository->findChildResult($result, $type);\n }\n\n // prophet API calls\n /**\n * @throws ApplicationException\n */\n public function getGenerateReportPayload(AutomatedReport $automatedReport, string $reportResultUuid): array\n {\n $period = $this->calculateFromAndToDate($automatedReport);\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n return [\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResultUuid,\n 'report_type' => $automatedReport->getType(),\n 'media_types' => $automatedReport->getMediaTypes(),\n 'from_date' => $fromDate->startOfDay()->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->endOfDay()->format(DateTimeInterface::RFC3339),\n 'group_ids' => $automatedReport->getGroups(),\n 'call_deal_stage' => $automatedReport->getDealAtCallStages(),\n 'current_deal_stage' => $automatedReport->getCurrentDealStages(),\n 'deal_min_value' => $automatedReport->getDealValueMin(),\n 'deal_max_value' => $automatedReport->getDealValueMax(),\n 'call_types' => $automatedReport->getCallTypes(),\n 'call_duration_min_seconds' => $automatedReport->getCallDurationMin(),\n 'call_duration_max_seconds' => $automatedReport->getCallDurationMax(),\n 'special_requirements' => $automatedReport->getAdditionalPromptInput(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->formatReportPeriodName(\n $automatedReport->getFrequency(),\n $fromDate,\n $toDate,\n ),\n 'playbook_categories' => $automatedReport->getPlaybookCategories(),\n 'custom_name' => $automatedReport->getCustomName(),\n ];\n }\n\n // $inputPayload - FE payload structure\n public function getActivitiesCountPayload(array $inputPayload): array\n {\n // Use validateAndTransformData to validate and normalize input\n $validatedData = $this->validateAndTransformData($inputPayload);\n $period = $this->calculateFromAndToDatePeriod(\n $validatedData['frequency'],\n Carbon::parse($validatedData['from']),\n Carbon::parse($validatedData['to']),\n );\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n // Create payload similar to getGenerateReportPayload\n return [\n 'team_id' => $validatedData['team_id'],\n 'group_ids' => $validatedData['groups'] ?? [],\n 'report_type' => $validatedData['type'],\n 'from_date' => $fromDate->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->format(DateTimeInterface::RFC3339),\n 'call_deal_stage' => $validatedData['deal_at_call_stages'] ?? [],\n 'current_deal_stage' => $validatedData['current_deal_stages'] ?? [],\n 'deal_min_value' => $validatedData['deal_value_min'] ?? null,\n 'deal_max_value' => $validatedData['deal_value_max'] ?? null,\n 'call_types' => $validatedData['call_types'],\n 'call_duration_min_seconds' => $validatedData['call_duration_min'] ?? null,\n 'call_duration_max_seconds' => $validatedData['call_duration_max'] ?? null,\n 'special_requirements' => $validatedData['additional_prompt_input'] ?? null,\n 'playbook_categories' => $validatedData['playbook_categories'] ?? [],\n 'request_id' => null,\n 'callback_url' => null,\n ];\n }\n\n public function shouldSendReport(array $users, ?CarbonInterface $generatedAt = null): bool\n {\n if (empty($users)) {\n return false;\n }\n\n $earliestTz = collect($users)\n ->mapWithKeys(function (array $user) {\n $tz = new DateTimeZone($user['timezone']);\n $nowUtc = new DateTime('now', new DateTimeZone('UTC'));\n $offset = $tz->getOffset($nowUtc);\n\n return [$user['timezone'] => $offset];\n })\n ->sortDesc()\n ->keys()\n ->first();\n\n $now = Carbon::now($earliestTz);\n $isScheduledTime = (int) $now->format('H') === self::SENT_REPORT_AT_HOURS;\n\n if ($isScheduledTime) {\n return true;\n }\n\n return $this->hasPassedScheduledTime($generatedAt, $earliestTz);\n }\n\n public function hasPassedScheduledTime(?CarbonInterface $generatedAt, string $timezone): bool\n {\n if ($generatedAt === null) {\n return false;\n }\n\n $now = Carbon::now($timezone);\n $scheduledTime = $now->copy()->setTime(self::SENT_REPORT_AT_HOURS, 0, 0);\n\n if ($now->hour < self::SENT_REPORT_AT_HOURS) {\n $scheduledTime = $scheduledTime->subDay();\n }\n\n $scheduledTimeUtc = $scheduledTime->copy()->utc();\n $generatedAtUtc = $generatedAt->copy()->utc();\n $nowUtc = $now->copy()->utc();\n\n return $generatedAtUtc->lt($scheduledTimeUtc) && $nowUtc->gt($scheduledTimeUtc);\n }\n\n public function calculateFromAndToDatePeriod(\n string $frequency,\n ?Carbon $fromDate = null,\n ?Carbon $toDate = null,\n DateTimeZone|string|null $timezone = 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($timezone);\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()->subWeek()->startOfWeek(CarbonInterface::MONDAY),\n 'toDate' => $now->copy()->subWeek()->endOfWeek(CarbonInterface::SUNDAY),\n ],\n self::FREQUENCY_MONTHLY => [\n 'fromDate' => $now->copy()->subMonthNoOverflow()->startOfMonth(),\n 'toDate' => $now->copy()->subMonthNoOverflow()->endOfMonth(),\n ],\n self::FREQUENCY_QUARTERLY => [\n 'fromDate' => $now->copy()->subQuarterNoOverflow()->startOfQuarter(),\n 'toDate' => $now->copy()->subQuarterNoOverflow()->endOfQuarter(),\n ],\n default => throw new InvalidArgumentException(\"Unsupported frequency: {$frequency}\"),\n };\n }\n\n private function calculateFromAndToDate(AutomatedReport $automatedReport): array\n {\n return $this->calculateFromAndToDatePeriod(\n $automatedReport->getFrequency(),\n $automatedReport->getFrom(),\n $automatedReport->getTo()\n );\n }\n\n public function getAskJiminnyGenerateReportPayload(\n AutomatedReport $automatedReport,\n AutomatedReportResult $reportResult,\n array $activityIds,\n ): array {\n return [\n 'user_question' => $automatedReport->getAskAnythingPrompt()?->getContent(),\n 'call_ids' => array_map('strval', $activityIds),\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResult->getUuid(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->getReportPeriodName($reportResult),\n 'report_name' => $automatedReport->getCustomName(),\n ];\n }\n\n private function getCallbackUrl(): string\n {\n return $this->webhookService->route('jiminny.webhook.reports.ready');\n }\n\n /**\n * Validate and transform payload data for automated reports\n *\n * @param array $data\n *\n * @throws InvalidArgumentException\n *\n * @return array\n */\n private function validateAndTransformData(array $data): array\n {\n // Validate organization (team) and check feature\n $team = $this->validateOrganization($data['organization'] ?? null);\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $type = $this->validateReportType($data['report_type'] ?? null);\n $frequency = $this->validateFrequency($data['frequency'] ?? null);\n $additionalPromptInput = $this->validateAdditionalPromptInput(\n $data['additional_prompt_input'] ?? null\n );\n $customReportName = $this->validateCustomReportName($data['custom_name'] ?? null);\n\n // Prepare data for the database\n $reportData = [\n 'team_id' => $team->getId(),\n 'type' => $type,\n 'status' => $status,\n 'frequency' => $frequency,\n 'additional_prompt_input' => $additionalPromptInput,\n 'custom_name' => $customReportName,\n ];\n\n // Validate deal values\n $reportData = $this->validateDealValues($data, $reportData);\n\n // Validate date range\n $reportData = $this->validateDateRange($data, $reportData, $frequency);\n\n // Validate call durations\n $reportData = $this->validateCallDurations($data, $reportData);\n\n // Validate call types\n $reportData = $this->validateCallTypes($data, $reportData);\n\n // Validate media types\n $reportData = $this->validateMediaTypes($data, $reportData);\n\n // Validate groups\n if (isset($data['teams'])) {\n $reportData['groups'] = $this->validateAndGetGroupIds($team, $data['teams']);\n }\n\n // Validate deal stages\n $reportData = $this->validateDealStages($data, $reportData, $team, $type);\n\n // Validate playbook categories\n $reportData = $this->validatePlaybookCategories($data, $reportData, $team);\n\n // Validate recipients\n $reportData['recipients'] = [\n 'users' => $this->validateAndGetUserIdsByTeam($team, $data['recipients'] ?? []),\n ];\n\n if (isset($data['jiminny_recipients'])) {\n // Validate Jiminny recipients\n $reportData['jiminny_recipients'] = [\n 'users' => $this->validateAndGetJiminnyUserIds((array) $data['jiminny_recipients']),\n ];\n }\n\n return $reportData;\n }\n\n private function validateDealValues(array $data, array $reportData): array\n {\n if (isset($data['min_deal_value'])) {\n $reportData['deal_value_min'] = (int) $data['min_deal_value'];\n\n if ($reportData['deal_value_min'] > 4294967295 || $reportData['deal_value_min'] < 0) {\n throw new InvalidArgumentException('Min deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_deal_value'])) {\n $reportData['deal_value_max'] = (int) $data['max_deal_value'];\n\n if ($reportData['deal_value_max'] > 4294967295 || $reportData['deal_value_max'] < 0) {\n throw new InvalidArgumentException('Max deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['min_deal_value'], $data['max_deal_value'])\n && $data['min_deal_value'] > $data['max_deal_value']\n ) {\n throw new InvalidArgumentException('Min deal value cannot be greater than max deal value');\n }\n\n return $reportData;\n }\n\n private function validateDateRange(array $data, array $reportData, string $frequency): array\n {\n // Set date range only for one_off frequency\n if ($frequency === 'one_off') {\n if (isset($data['start_date_period'])) {\n $reportData['from'] = $this->parseDate($data['start_date_period']);\n }\n\n if (isset($data['end_date_period'])) {\n $reportData['to'] = $this->parseDate($data['end_date_period']);\n }\n\n if (empty($reportData['from']) || empty($reportData['to'])) {\n throw new InvalidArgumentException(\n 'Start date and end date are required for one_off frequency'\n );\n }\n } else {\n $reportData['from'] = null;\n $reportData['to'] = null;\n }\n\n return $reportData;\n }\n\n private function validateCallDurations(array $data, array $reportData): array\n {\n // Convert call durations from minutes to seconds\n if (isset($data['min_call_duration'])) {\n $reportData['call_duration_min'] = (int) $data['min_call_duration'] * 60;\n\n if ($reportData['call_duration_min'] > 4294967295 || $reportData['call_duration_min'] < 0) {\n throw new InvalidArgumentException('Min call duration should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_call_duration'])) {\n $reportData['call_duration_max'] = (int) $data['max_call_duration'] * 60;\n\n if ($reportData['call_duration_max'] > 4294967295 || $reportData['call_duration_max'] < 0) {\n throw new InvalidArgumentException('Max call duration should be between 0 and 4294967295');\n }\n }\n\n return $reportData;\n }\n\n private function validateCallTypes(array $data, array $reportData): array\n {\n // Set call types\n $reportData['call_types'] = $data['call_type'] ?? [];\n if (empty($reportData['call_types'])) {\n $reportData['call_types'] = self::getCallTypes();\n }\n\n foreach ($reportData['call_types'] as $callType) {\n if (! in_array($callType, self::getCallTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Call type %s is invalid', $callType));\n }\n }\n\n return $reportData;\n }\n\n private function validateMediaTypes(array $data, array $reportData): array\n {\n // Set media types from input data\n $reportData['media_types'] = $data['media_types'] ?? [];\n\n if (empty($reportData['media_types'])) {\n throw new InvalidArgumentException('Media types are required');\n }\n\n foreach ($reportData['media_types'] as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n throw new InvalidArgumentException(sprintf('Media type %s is invalid', $mediaType));\n }\n }\n\n return $reportData;\n }\n\n private function validateDealStages(array $data, array $reportData, Team $team, string $reportType): array\n {\n // Validate and set deal stages\n if (isset($data['deal_stage_at_call'])) {\n $reportData['deal_at_call_stages'] =\n $this->validateAndGetDealStageIds($team, $data['deal_stage_at_call'], 'Deal stage at call');\n }\n\n if (isset($data['current_deal_stage'])) {\n $reportData['current_deal_stages'] =\n $this->validateAndGetDealStageIds($team, $data['current_deal_stage'], 'Current deal stage');\n }\n\n // Ensure current_deal_stage is not provided for loss_analysis report type\n if ($reportType === self::TYPE_LOSS_ANALYSIS && ! empty($data['current_deal_stage'])) {\n throw new InvalidArgumentException('Current deal stage is not applicable for Loss Analysis reports');\n }\n\n return $reportData;\n }\n\n // transform uuid to id\n private function validatePlaybookCategories(array $data, array $reportData, Team $team): array\n {\n $key = 'playbook_categories';\n\n if (isset($data[$key])) {\n $payloadIds = $data[$key];\n $ids = [];\n\n foreach ($payloadIds as $uuid) {\n $uuid = (string) $uuid;\n\n try {\n $playbookCategory = $this->playbookCategoryRepository->findByUuid($uuid);\n } catch (Throwable $throwable) {\n Log::error(__METHOD__ . ' ' . $throwable->getMessage());\n\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory) {\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory->hasPlaybook()) {\n throw new InvalidArgumentException(sprintf('Playbook category %s has no playbook', $uuid));\n }\n\n if ($playbookCategory->getPlaybook()->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Playbook category %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $ids[] = $playbookCategory->getId();\n }\n\n $reportData[$key] = $ids;\n }\n\n return $reportData;\n }\n\n private function validateReportStatus($status): bool\n {\n if (! in_array($status, [true, false], true)) {\n throw new InvalidArgumentException('Report status is invalid');\n }\n\n return $status;\n }\n\n private function validateReportType($type): string\n {\n if (! in_array($type, self::getTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Report type is invalid: %s', $type));\n }\n\n return $type;\n }\n\n private function validateFrequency($frequency): string\n {\n if (! in_array($frequency, self::getFrequencies(), true)) {\n throw new InvalidArgumentException('Frequency is invalid');\n }\n\n return $frequency;\n }\n\n private function validateAdditionalPromptInput(?string $additionalPromptInput): ?string\n {\n if ($additionalPromptInput && strlen($additionalPromptInput) > 5000) {\n throw new InvalidArgumentException('Additional Prompt Input should be less than 5000 characters');\n }\n\n return $additionalPromptInput;\n }\n\n private function validateCustomReportName(?string $customReportName): ?string\n {\n if ($customReportName === null || $customReportName === '') {\n return null;\n }\n\n if (strlen($customReportName) > 70) {\n throw new InvalidArgumentException('Custom report name should be less than 70 characters');\n }\n\n return $customReportName;\n }\n\n private function validateOrganization(?string $organizationUuid): Team\n {\n if (! $organizationUuid) {\n throw new InvalidArgumentException('Organization is required');\n }\n\n $team = $this->teamRepository->idOrUuid($organizationUuid);\n\n if (! $team) {\n throw new InvalidArgumentException('Organization not found');\n }\n\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n throw new InvalidArgumentException('Organization does not have the Automated Reports feature');\n }\n\n return $team;\n }\n\n private function validateAndGetGroupIds(Team $team, array $teamUuids): array\n {\n $groupIds = [];\n\n foreach ($teamUuids as $uuid) {\n $group = $this->groupRepository->findByUuid($uuid);\n\n if ($group === null || $group->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Group %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $groupIds[] = $group->getId();\n\n }\n\n return $groupIds;\n }\n\n private function validateAndGetDealStageIds(Team $team, array $stageUuids, string $propertyLabel): array\n {\n $stageIds = [];\n\n foreach ($stageUuids as $uuid) {\n $stage = $this->stageRepository->findByUuid($uuid);\n\n if ($stage === null || $stage->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Stage %s not found for team %s for %s', $uuid, $team->getUuid(), $propertyLabel)\n );\n }\n\n $stageIds[] = $stage->getId();\n }\n\n return $stageIds;\n }\n\n private function validateAndGetUserIds(array $userUuids, callable $teamCheck): array\n {\n if (empty($userUuids)) {\n return [];\n }\n\n $userIds = [];\n\n foreach ($userUuids as $uuid) {\n $user = $this->userRepository->findByUuid($uuid);\n\n if (! $user || ! $user->isStatusActive()) {\n throw new InvalidArgumentException(\n sprintf('User %s not found or is not active', $uuid)\n );\n }\n\n if (! $teamCheck($user)) {\n throw new InvalidArgumentException(\n sprintf('User %s does not belong to the allowed team(s)', $uuid)\n );\n }\n\n $userIds[] = $user->getId();\n }\n\n return $userIds;\n }\n\n private function validateAndGetUserIdsByTeam(Team $team, array $userUuids): array\n {\n return $this->validateAndGetUserIds($userUuids, fn ($user) => $user->getTeamId() === $team->getId());\n }\n\n private function validateAndGetJiminnyUserIds(array $userUuids): array\n {\n $allowedTeamIds = config('kiosk.teamIds', []);\n\n return $this->validateAndGetUserIds($userUuids, fn ($user) => in_array($user->getTeamId(), $allowedTeamIds, true));\n }\n\n private function parseDate(string $dateString): string\n {\n return date('Y-m-d H:i:s', strtotime($dateString));\n }\n\n private function generateReportResultViewUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.view', ['uuid' => $result->getUuid()]);\n }\n\n private function generateReportResultDownloadUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.download', ['uuid' => $result->getUuid()]);\n }\n\n private function getReportMediaRouteResource(AutomatedReportResult $result): string\n {\n if ($result->getMediaType() === self::MEDIA_TYPE_PDF) {\n return self::PDF_KEY;\n } elseif ($result->getMediaType() === self::MEDIA_TYPE_PODCAST) {\n return self::AUDIO_KEY;\n }\n\n throw new \\InvalidArgumentException('Unknown media type.');\n }\n\n public function getMediaPath(AutomatedReportResult $result): ?string\n {\n $url = match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => $result->getPdfUrl(),\n self::MEDIA_TYPE_PODCAST => $result->getPodcastAudioUrl(),\n default => null,\n };\n\n if ($url === null) {\n return null;\n }\n\n $path = parse_url(trim($url, '\"\\''), PHP_URL_PATH);\n\n return $path ?: null;\n }\n\n public function getFilenameSuffix(AutomatedReportResult $result): ?string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => 'Podcast',\n default => null,\n };\n }\n\n public function getMailSubjectSuffix(AutomatedReportResult $result): string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => 'report',\n self::MEDIA_TYPE_PODCAST => 'podcast',\n default => '',\n };\n }\n\n public function getMediaTypeMetadata(AutomatedReportResult $result): array\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => ['extension' => 'mp3', 'mime' => 'audio/mpeg'],\n self::MEDIA_TYPE_PDF => ['extension' => 'pdf', 'mime' => 'application/pdf'],\n default => ['extension' => null, 'mime' => null],\n };\n }\n\n public function deleteS3Files(AutomatedReportResult $result): void\n {\n $teamUuid = $result->getReport()->getTeam()->getUuid();\n $reportUuid = $result->getUuid();\n\n // delete all files for a report uuid no mather of pdf, podcast, or both\n // in case of both - the podcast files are linked to the pdf (parent) uuid\n // pdf and podcast date times should be close\n $path = sprintf('%s/%s/%s', $teamUuid, self::S3_DIR, $reportUuid);\n\n foreach (self::FILE_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted S3 file', [\n 'path' => $file,\n ]);\n }\n }\n\n foreach (self::FILE_PODCAST_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '_podcast.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted Podcast S3 file', [\n 'path' => $file,\n ]);\n }\n }\n }\n\n /**\n *\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): Collection\n {\n return $this->automatedReportsRepository->getTeamIdsWithReportsResults($teamId);\n }\n\n /**\n * Core delete logic for report results using a query\n *\n * @param Builder $query\n * @param array $logContext\n *\n * @return int\n */\n private function deleteReportResultsByQuery(Builder $query, array $logContext = []): int\n {\n $deletedCount = 0;\n\n if ($query->exists()) {\n Log::info(\n 'Run delete report results',\n array_merge(\n $logContext,\n [\n 'service' => 'AutomatedReportsService',\n ]\n )\n );\n\n $query->chunkById(50, function ($results) use (&$deletedCount, $logContext) {\n foreach ($results as $result) {\n $this->deleteReportResult($result);\n $deletedCount++;\n\n Log::info(\n 'Deleted a report result',\n array_merge(\n $logContext,\n [\n 'result_id' => $result->getId(),\n 'report_id' => $result->getReportId(),\n ]\n )\n );\n }\n });\n }\n\n return $deletedCount;\n }\n\n /**\n * Delete report results for a team by retention period\n *\n * @param Team $team\n * @param CarbonImmutable $retentionDate\n *\n * @return int Number of deleted report results\n */\n public function deleteReportsResultsInRetentionPeriod(Team $team, CarbonImmutable $retentionDate): int\n {\n $reportIds = $this->automatedReportsRepository->getReportIdsByTeam($team);\n\n if ($reportIds->isEmpty()) {\n return 0;\n }\n\n $query = $this->automatedReportsRepository\n ->getReportResultsQueryForRetention($team, $retentionDate);\n\n return $this->deleteReportResultsByQuery($query, [\n 'team_id' => $team->getId(),\n 'retention_date' => $retentionDate->toDateTimeString(),\n ]);\n }\n\n /**\n * Delete ALL report results for a specific automated report\n *\n * @param string $uuid\n *\n * @return int\n */\n public function deleteReportResults(string $uuid): int\n {\n $report = $this->getReport($uuid);\n\n $query = $this->automatedReportsRepository->getResultsByReportQuery($report);\n\n return $this->deleteReportResultsByQuery($query, [\n 'report_uuid' => $uuid,\n 'report_id' => $report->getId(),\n ]);\n }\n\n public function deleteReportResult(AutomatedReportResult $result): void\n {\n $this->deleteS3Files($result);\n\n $result->delete();\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getTeamReports(Team $team): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getReportsByTeam($team);\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getReportResults(AutomatedReport $report): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getResultsByReport($report);\n }\n\n public function deleteAllReportResults(AutomatedReport $report): void\n {\n $results = $this->getReportResults($report);\n\n /** @var AutomatedReportResult $result */\n foreach ($results as $result) {\n Log::info('Deleting result', [\n 'report' => $report->getId(),\n 'result' => $result->getId(),\n ]);\n\n $this->deleteReportResult($result);\n }\n }\n\n public function deleteAllData(Team $team): void\n {\n Log::info('Deleting automated report and results for team', [\n 'team' => $team->getId(),\n ]);\n\n $reports = $this->getTeamReports($team);\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n Log::info('Deleting report', [\n 'team' => $team->getId(),\n 'report' => $report->getId(),\n ]);\n\n $this->deleteAllReportResults($report);\n\n $report->delete();\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.42785904,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.43650267,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.4474734,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.45611703,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.46476063,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.47573137,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.4867021,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"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.51329786,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.5242686,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.70611703,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1751808696220340334
|
1126710648141684156
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskAnythingPromptServiceTest
Run 'AskAnythingPromptServiceTest'
Debug 'AskAnythingPromptServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
100
3
34
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Kiosk\AutomatedReports;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Carbon\Exceptions\InvalidFormatException;
use DateTime;
use DateTimeInterface;
use DateTimeZone;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\ActivitySearch\FilterDefinition\InputTypeEnum;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Partner;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Throwable;
class AutomatedReportsService
{
public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';
public const string TYPE_ASK_JIMINNY = 'ask_jiminny';
/**
* Standard report types (used by kiosk for existing automated reports).
*/
// @TODO this will add filter, however if we need to control feature by FF we need conditional logic
public const array TYPES = [
['id' => 'exec_summary', 'name' => 'Exec Summary'],
['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],
['id' => 'product_feedback', 'name' => 'Product Feedback'],
['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],
// ['id' => 'questions', 'name' => 'Questions'],
// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],
];
public const array ALL_TYPES = [
...self::TYPES,
['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],
];
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';
/**
* Frequencies for standard (non-Ask Jiminny) reports.
*/
public const array FREQUENCIES = [
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
/**
* Frequencies for Ask Jiminny reports.
*/
public const array ASK_JIMINNY_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
];
public const string MEDIA_TYPE_PDF = 'pdf';
public const string MEDIA_TYPE_PODCAST = 'podcast';
public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];
public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];
public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];
public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];
public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];
public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];
public const int SENT_REPORT_AT_HOURS = 5;
public const string PDF_KEY = 'pdf';
public const string AUDIO_KEY = 'audio';
private const array ALL_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
private const string S3_DIR = 'reports';
private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];
private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly GroupRepository $groupRepository,
private readonly UserRepository $userRepository,
private readonly StageRepository $stageRepository,
private readonly DealStagesService $dealStagesService,
private readonly RecipientsService $recipientsService,
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly Webhook $webhookService,
private readonly BusDispatcher $dispatcher,
private readonly ActivityTypeService $activityTypeService,
private readonly PlaybookCategoryRepository $playbookCategoryRepository,
private readonly AskAnythingPromptService $askAnythingPromptService,
private readonly SearchRepository $activitySearchRepository,
private readonly AskAnythingRepository $askAnythingRepository,
) {
}
public static function getTypes(): array
{
$types = self::TYPES;
return array_map(static function ($type) {
return $type['id'];
}, $types);
}
public static function getCallTypes(): array
{
return array_map(static function ($callType) {
return $callType['id'];
}, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);
}
public static function getFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::FREQUENCIES);
}
// front-facing structure
public function getReportEnabledFieldData(bool $value = false): array
{
return [
'id' => 'report_enabled',
'label' => '',
'inputType' => InputTypeEnum::TOGGLE,
'value' => $value,
];
}
// Organizations = Teams
public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array
{
$options = $this->getTeams(partner: $partner);
if ($shortVersion) {
return [
'id' => 'organization',
'label' => 'Organization',
'options' => $options,
];
}
return [
'id' => 'organization',
'label' => 'Organization',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $options,
'value' => $value,
'dependencies' => [
'teams',
'deal_stage_at_call',
'current_deal_stage',
'recipients',
ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,
],
'dependsOn' => [],
];
}
// Teams = Groups
public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array
{
if ($shortVersion) {
return [
'id' => 'teams',
'label' => 'Team',
'options' => $options,
];
}
return [
'id' => 'teams',
'label' => 'Team',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => false,
'placeholder' => 'Select',
'options' => $options,
'value' => $value, // value should be an array of objects {id, name}
'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],
'dependsOn' => [],
];
}
public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array
{
$types = [];
if ($team instanceof Team) {
if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
$types = self::TYPES;
}
if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {
$types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];
}
} else {
$types = self::TYPES;
}
if ($shortVersion) {
return [
'id' => 'report_type',
'label' => 'Report Type',
'options' => $types,
];
}
return [
'id' => 'report_type',
'label' => 'Report Type',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $types,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getFrequencyFieldData(?string $value = null): array
{
return [
'id' => 'frequency',
'label' => 'Frequency',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => self::FREQUENCIES,
'value' => $value,
'dependencies' => ['period'],
'dependsOn' => [],
];
}
public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array
{
return [
'id' => 'period',
'label' => 'Select one-off period',
'inputType' => InputTypeEnum::DATE_RANGE,
'required' => true,
'placeholder' => 'Select',
'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],
'queryParams' => [
'startDate' => 'start_date_period',
'endDate' => 'end_date_period',
],
'dependencies' => [],
'dependsOn' => ['frequency'],
];
}
public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array
{
return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);
}
public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);
}
public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);
}
public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'deal_value',
'label' => 'Deal Value',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_deal_value',
'max' => 'max_deal_value',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array
{
$value = [];
$conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;
$dialerOn && $value[] = self::CALL_TYPE_DIALER;
return [
'id' => 'call_type',
'label' => 'Call Type',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => [
self::CALL_TYPE_CONFERENCE,
self::CALL_TYPE_DIALER,
],
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getMediaTypeFieldData(?AutomatedReport $report = null): array
{
$value = [];
if ($report) {
$value = $this->transformMediaTypes($report);
}
return [
'id' => 'media_types',
'label' => 'Export as',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => self::MEDIA_TYPE_OBJECTS,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'call_duration',
'label' => 'Call Duration',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_call_duration',
'max' => 'max_call_duration',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getRecipientsFieldData(?Team $team = null, array $value = []): array
{
return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);
}
public function getJiminnyRecipientsFieldData(array $value = []): array
{
return $this->recipientsService->getJiminnyRecipientsFieldData($value);
}
public function getAdditionalPromptInputFieldData(?string $value = null): array
{
return [
'id' => 'additional_prompt_input',
'label' => 'Special requirements',
'inputType' => InputTypeEnum::TEXTAREA,
'required' => false,
'placeholder' => 'What should be the focus of the report?',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCustomReportNameFieldData(?string $value = null): array
{
return [
'id' => 'custom_name',
'label' => 'Custom report name',
'inputType' => InputTypeEnum::TEXT,
'required' => false,
'placeholder' => 'Enter custom name',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
// data providers
public function getTeams(?Partner $partner = null): array
{
$teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);
$teamData = [];
foreach ($teams as $team) {
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
continue;
}
$teamData[] = $this->transformTeam($team);
}
return $teamData;
}
public function getTeamGroups(string $teamUuid): array
{
$data = [];
$team = $this->getTeam($teamUuid);
if ($team !== null) {
$groups = $team->groups()->get();
foreach ($groups as $group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
];
}
}
return $data;
}
public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array
{
$data = [];
$teams = $this->getTeams(partner: $partner);
foreach ($teams as $team) {
if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {
continue;
}
$data[] = [
'label' => $team['name'],
'groups' => $this->getTeamGroups($team['id']),
];
}
return $data;
}
public function getTeam(string $teamUuid): ?Team
{
return $this->teamRepository->idOrUuid($teamUuid);
}
public function getTeamById(int $teamId): ?Team
{
return $this->teamRepository->find($teamId);
}
public function getGroupsUuids(AutomatedReport $report): array
{
$uuids = [];
$reportGroups = $report->getGroups();
foreach ($reportGroups as $groupId) {
if ($group = $this->groupRepository->find($groupId)) {
$uuids[] = $group->getUuid();
}
}
return $uuids;
}
public function getPlaybookCategoriesUuids(AutomatedReport $report): array
{
$uuids = [];
$playbookCategories = $report->getPlaybookCategories();
foreach ($playbookCategories as $id) {
if ($category = $this->playbookCategoryRepository->find($id)) {
$uuids[] = $category->getUuid();
}
}
return $uuids;
}
public function getDealAtCallStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getDealAtCallStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getCurrentDealStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getCurrentDealStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getRecipients());
}
public function getJiminnyUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function extractUserUuids(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => $user->getUuid())
->values()
->all();
}
// get mail data
public function getRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getRecipients());
}
/**
* @return array<UserContract>
*/
public function getRecipientUserObjects(AutomatedReport $report): array
{
$userIds = $report->getRecipients()['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->values()
->all();
}
private function getJiminnyRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function buildRecipientUsers(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => [
'email' => $user->getEmailAddress(),
'name' => $user->getName(),
'timezone' => $user->getTimezone()->getName(),
])
->values()
->all();
}
public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array
{
if ($report->isAskJiminnyReport()) {
$recipients = $this->resolveAskJiminnyRecipients($report);
} else {
$recipients = $this->getRecipientUsers($report);
if ($includeJiminny) {
$recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));
}
}
$emails = [];
return array_values(array_filter(
$recipients,
static function ($recipient) use (&$emails) {
if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {
return false;
}
$emails[] = $recipient['email'];
return true;
}
));
}
private function resolveAskJiminnyRecipients(AutomatedReport $report): array
{
$recipients = [];
$creator = $report->getCreator();
if ($creator !== null) {
$recipients[] = [
'email' => $creator->getEmailAddress(),
'name' => $creator->getName(),
'timezone' => $creator->getTimezone()->getName(),
];
}
return array_merge(
$recipients,
$this->buildRecipientUsers($report->getRecipients()),
$this->getGroupRecipientUsers($report),
);
}
private function getGroupRecipientUsers(AutomatedReport $report): array
{
$users = [];
foreach ($report->getGroups() as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group === null) {
continue;
}
foreach ($group->getMembers() as $member) {
$users[] = [
'email' => $member->getEmailAddress(),
'name' => $member->getName(),
'timezone' => $member->getTimezone()->getName(),
];
}
}
return $users;
}
public function getReportTypeName(AutomatedReportResult $report): string
{
$type = $report->getReport()->getType();
$getType = $this->transformReportType($type);
return $getType['name'];
}
public function getReportPeriodName(AutomatedReportResult $report): string
{
$from = $report->getFromDate();
$to = $report->getToDate();
$frequency = $report->getReport()->getFrequency();
if ($from === null || $to === null) {
if (! $report->getReport()->isAskJiminnyReport()) {
$invalidPeriod = $from === null ? 'from' : 'to';
throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);
}
$timezone = $report->getReport()->getCreator()?->getTimezone();
$period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);
$from = $period['fromDate'];
$to = $period['toDate'];
}
return $this->formatReportPeriodName($frequency, $from, $to);
}
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 getReportTeamsName(AutomatedReportResult $report): string
{
$groups = $report->getGroups();
if (empty($groups)) {
return 'All';
}
// Get group names from repository
$groupNames = [];
foreach ($groups as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group) {
$groupNames[] = $group->getName();
}
}
if (count($groupNames) === 1) {
// Single team format
$teamsName = $groupNames[0];
} else {
// Multiple teams format
$teamsName = implode(', ', $groupNames);
}
return $teamsName;
}
public function getReportFileName(AutomatedReportResult $report): string
{
$customName = $report->getReport()->getCustomName();
$periodName = $this->getReportPeriodName($report);
$filenameSuffix = $this->getFilenameSuffix($report);
if ($customName) {
if ($filenameSuffix) {
$customName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$customName} - {$periodName}");
}
$baseName = $this->getReportTypeName($report);
if ($filenameSuffix) {
$baseName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}");
}
public function getReportFileNameWithExtension(AutomatedReportResult $result): string
{
$extension = $this->getMediaTypeMetadata($result)['extension'];
return $this->getReportFileName($result) . '.' . $extension;
}
public function sanitizeFileName(string $fileName): string
{
return str_replace(['/', '\\'], '-', $fileName);
}
public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool
{
$recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);
if (in_array($user->getId(), $recipientIds, true)) {
return true;
}
if ($report->isAskJiminnyReport()) {
$groupId = $user->getGroupId();
if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {
return true;
}
}
return false;
}
public function transformReportResults(Collection $automatedReportResults): array
{
$data = [];
foreach ($automatedReportResults as $automatedReportResult) {
/** @var AutomatedReportResult $automatedReportResult */
$report = $automatedReportResult->getReport();
$createdBy = $report->getCreator();
$creator = [
'id' => $createdBy?->getUuid(),
'name' => $createdBy?->getName(),
'email' => $createdBy?->getEmailAddress(),
'photoUrl' => $createdBy?->getPhotoUrl(),
];
$data[] = [
'id' => $automatedReportResult->getUuid(),
'name' => $automatedReportResult->getName(),
'frequency' => $this->transformFrequency($report->getFrequency()),
'recipients' => $this->buildRecipients($report),
'report_type' => $this->transformReportType($report->getType()),
'media_type' => $automatedReportResult->getMediaType(),
'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),
'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),
'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),
'creator' => $creator,
];
}
return $data;
}
private function buildRecipients(AutomatedReport $report): array
{
$creatorUuid = $report->getCreator()?->getUuid();
$recipients = array_values(array_filter(
$this->transformRecipients($report->getRecipients()),
static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,
));
if (! $report->isAskJiminnyReport()) {
return $recipients;
}
return [
...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),
...$recipients,
];
}
public function hasCallTypeConference(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);
}
public function hasCallTypeDialer(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);
}
// transformers
private function transformTeam(Team $team): array
{
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
return [];
}
return [
'id' => $team->getUuid(),
'name' => $team->getName(),
];
}
private function transformReportFullView(AutomatedReport $report): array
{
$base = $this->transformReportBase($report);
return $report->getType() === self::TYPE_ASK_JIMINNY
? $base + $this->transformAskJiminnyFields($report)
: $base + $this->transformStandardReportFields($report);
}
private function transformReportBase(AutomatedReport $report): array
{
return [
'id' => $report->getUuid(),
'organization' => $this->transformOrganization(team: $report->getTeam()),
'report_type' => $this->transformReportType($report->getType()),
'frequency' => $this->transformFrequency($report->getFrequency()),
];
}
private function transformStandardReportFields(AutomatedReport $report): array
{
$team = $report->getTeam();
return [
'report_enabled' => $report->getStatus(),
'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),
'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),
'deal_value_min' => $report->getDealValueMin(),
'deal_value_max' => $report->getDealValueMax(),
'call_types' => $this->transformCallType($report->getCallTypes()),
'media_types' => $this->transformMediaTypes($report),
'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),
'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),
'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),
'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),
'recipients' => $this->transformRecipients($report->getRecipients()),
'created_by' => $this->transformCreator($report->getCreator()),
'additional_prompt_input' => $report->getAdditionalPromptInput(),
'custom_name' => $report->getCustomName(),
'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),
'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),
'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),
];
}
private function transformAskJiminnyFields(AutomatedReport $report): array
{
$team = $report->getTeam();
$creatorId = $report->getAttribute('created_by');
$explicitUserIds = array_values(array_filter(
$report->getRecipients()['users'] ?? [],
static fn ($id) => $id !== $creatorId
));
return [
'report_name' => $report->getCustomName(),
'enabled' => $report->getStatus(),
'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),
'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),
'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),
'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),
];
}
private function transformOrganization(?Team $team): array
{
return [
'id' => $team?->getUuid(),
'name' => $team?->getName(),
];
}
private function transformReportType(string $type): array
{
foreach (self::ALL_TYPES as $typeItem) {
if ($typeItem['id'] === $type) {
return $typeItem;
}
}
return [];
}
private function transformCallType(array $types): array
{
$result = [];
$callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];
foreach ($types as $type) {
foreach ($callTypes as $callTypeItem) {
if ($callTypeItem['id'] === $type) {
$result[] = $callTypeItem;
break;
}
}
}
return $result;
}
private function transformMediaTypes(AutomatedReport $report): array
{
$values = [];
foreach ($report->getMediaTypes() as $mediaType) {
if (! in_array($mediaType, self::MEDIA_TYPES, true)) {
continue;
}
$values[] = match ($mediaType) {
self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,
self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,
};
}
return $values;
}
private function transformFrequency(string $frequency): array
{
foreach (self::ALL_FREQUENCIES as $frequencyItem) {
if ($frequencyItem['id'] === $frequency) {
return $frequencyItem;
}
}
return [];
}
public function transformDurationToMinutes(?int $duration): ?int
{
if (! $duration) {
return null;
}
return (int) ($duration / 60);
}
private function transformGroups(?Team $team, array $groupsIds): array
{
if (empty($groupsIds) || ! $team) {
return [];
}
$data = [];
foreach ($groupsIds as $groupId) {
$group = $team->groups()->where('id', $groupId)->first();
if ($group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
'photoUrl' => $group->getPhotoUrl(),
];
}
}
return $data;
}
private function transformStages(?Team $team, array $stagesIds): array
{
if (empty($stagesIds) || ! $team) {
return [];
}
$data = [];
foreach ($stagesIds as $stageId) {
$stage = $team->stages()->where('id', $stageId)->first();
if ($stage) {
$data[] = [
'id' => $stage->getUuid(),
'name' => $stage->getName(),
];
}
}
return $data;
}
private function transformRecipients(array $recipients): array
{
$users = [];
foreach ($recipients['users'] ?? [] as $userId) {
$users[] = $this->transformUser($userId);
}
return $users;
}
private function transformCreator(?User $user): ?array
{
if ($user === null) {
return null;
}
return $this->transformUser($user->getId());
}
private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array
{
if ($prompt === null) {
return null;
}
return [
'id' => $prompt->getUuid(),
'name' => $prompt->getTitle(),
];
}
private function transformSafeSearch(?Search $search): ?array
{
if ($search === null) {
return null;
}
return [
'id' => $search->getUuid(),
'name' => $search->getName(),
];
}
private function transformUser(int $userId): array
{
/* @var ?User $user */
$user = $this->userRepository->find($userId);
return [
'id' => $user?->getUuid(),
'name' => $user?->getName(),
'email' => $user?->getEmailAddress(),
'photoUrl' => $user?->getPhotoUrl(),
];
}
public function create(array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$validatedData['created_by'] = auth()->id();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
public function update(string $uuid, array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$report = $this->automatedReportsRepository->findByUuid($uuid);
if (! $report) {
throw new InvalidArgumentException('Report not found');
}
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
/**
* Create an Ask Jiminny report.
*/
public function createAskJiminnyReport(array $data, User $creator): array
{
$validatedData = $this->validateAskJiminnyReportData($data, $creator);
$validatedData['created_by'] = $creator->getId();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
return $this->transformReportFullView($automatedReport);
}
/**
* Update an Ask Jiminny report.
*/
public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array
{
if (! $report->isAskJiminnyReport()) {
throw new InvalidArgumentException('Report is not an Ask Jiminny report');
}
$validatedData = $this->validateAskJiminnyReportData($data, $user);
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
return $this->transformReportFullView($automatedReport);
}
public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array
{
if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {
throw new InvalidArgumentException(
'This report is missing a saved search or prompt. ' .
'Edit the report to complete the setup before enabling it.'
);
}
$this->automatedReportsRepository->update($report, ['status' => $status]);
return $this->transformReportFullView($report->fresh());
}
/**
* Validate and transform data for Ask Jiminny reports.
*/
private function validateAskJiminnyReportData(array $data, User $user): array
{
// Validate name
$name = trim($data['report_name'] ?? '');
if (empty($name)) {
throw new InvalidArgumentException('Report name is required');
}
if (mb_strlen($name) > 50) {
throw new InvalidArgumentException('Report name must be 50 characters or less');
}
// Validate frequency (only daily, weekly, monthly for Ask Jiminny)
$frequency = $data['frequency'] ?? null;
$askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];
if (! in_array($frequency, $askJiminnyFrequencies, true)) {
throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');
}
// Validate expiration date
$expiresAt = $data['expires_on'] ?? null;
if (empty($expiresAt)) {
throw new InvalidArgumentException('Expiration date is required');
}
try {
$expiresAtDate = Carbon::parse($expiresAt);
} catch (InvalidFormatException $e) {
throw new InvalidArgumentException('Expiration date format is invalid');
}
$maxExpiration = Carbon::now()->addYear()->endOfDay();
if ($expiresAtDate->gt($maxExpiration)) {
throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');
}
if ($expiresAtDate->isPast()) {
throw new InvalidArgumentException('Expiration date cannot be in the past');
}
// Validate saved search
$activitySearchId = $data['saved_search'] ?? null;
if (empty($activitySearchId)) {
throw new InvalidArgumentException('Saved search is required');
}
$savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);
if (! $savedSearch) {
throw new InvalidArgumentException('Saved search not found or does not belong to you');
}
// Validate saved prompt
$askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;
if (empty($askAnythingPromptId)) {
throw new InvalidArgumentException('Ask Jiminny prompt is required');
}
$prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);
if (! $prompt) {
throw new InvalidArgumentException('Ask Jiminny prompt not found');
}
// Validate status
$status = $data['enabled'] ?? false;
$recipientUserIds = [$user->getId()];
if (! empty($data['share_users'])) {
$sharedUserIds = $this->validateAndGetUserIdsByTeam(
$user->team,
(array) $data['share_users']
);
$recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);
}
$sharedGroupIds = [];
if (! empty($data['share_teams'])) {
$sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);
}
$recipientUserIds = array_values(array_unique($recipientUserIds));
return [
'team_id' => $user->getTeamId(),
'type' => self::TYPE_ASK_JIMINNY,
'status' => (bool) $status,
'frequency' => $frequency,
'custom_name' => $name,
'activity_search_id' => $savedSearch->getId(),
'ask_anything_prompt_id' => $prompt->getId(),
'expires_at' => $expiresAtDate->toDateString(),
'media_types' => [self::MEDIA_TYPE_PDF],
'call_types' => [],
'recipients' => ['users' => $recipientUserIds],
'groups' => $sharedGroupIds,
];
}
public static function getAskJiminnyFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::ASK_JIMINNY_FREQUENCIES);
}
public function getAskJiminnyReportFilters(User $user): array
{
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
return [
[
'id' => 'prompt',
'label' => 'Prompt',
'options' => $prompts,
],
[
'id' => 'saved_search',
'label' => 'Saved Search',
'options' => $savedSearches,
],
];
}
public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array
{
$team = $user->getTeam();
$userTimezone = $user->getTimezone();
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
$teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [
'id' => $group->getUuid(),
'name' => $group->getName(),
])->values()->all();
$shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];
$sharedTeamsValue = [];
$sharedUsersValue = [];
if ($report) {
$sharedTeamsValue = $this->transformGroups($team, $report->getGroups());
$recipientUserIds = $report->getRecipients()['users'] ?? [];
$creatorId = $report->getAttribute('created_by');
$sharedUserIds = array_values(array_filter(
$recipientUserIds,
static fn ($id) => $id !== $creatorId
));
$sharedUsersValue = collect($sharedUserIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (User $u) => [
'id' => $u->getUuid(),
'name' => $u->getName(),
])
->values()
->all();
}
return [
'fields' => [
[
'id' => 'enabled',
'inputType' => InputTypeEnum::TOGGLE,
'label' => '',
'value' => $report?->getStatus() ?? false,
],
[
'id' => 'report_name',
'inputType' => InputTypeEnum::TEXT,
'label' => 'Name',
'placeholder' => 'Enter name',
'required' => true,
'validation' => ['maxLength' => 50],
'value' => $report?->getCustomName() ?? '',
...
|
57993
|
NULL
|
NULL
|
NULL
|
|
58023
|
2042
|
4
|
2026-05-19T11:28:14.086506+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190094086_m1.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1D SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1DEV (docker)₴82APP (-zslreturn new JsonResponse(['error'Serror],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLaterMore>0.(ahlSupport Daily • in 32 mJiminny ...scnicre# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...100% C7Tue 19 May 14:28:13ED→Describe what you are looking forNikolay Yankov6 0• Messages+. Direct messages8. Nikolay Yankov. Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomova Todor Stamatov *Mario Georgievao Nikolay Ivanovdo James GrahamStoyan TanevLukas Kovalik y... Ol:: AppsJira CloudToastAdd canvas@ Files/api/v2/u3 new messagests?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcalliзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message Serror = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNewNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Message Nikolay Yankov+...
|
NULL
|
-4269388533221468316
|
NULL
|
idle
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1D SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1DEV (docker)₴82APP (-zslreturn new JsonResponse(['error'Serror],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLaterMore>0.(ahlSupport Daily • in 32 mJiminny ...scnicre# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...100% C7Tue 19 May 14:28:13ED→Describe what you are looking forNikolay Yankov6 0• Messages+. Direct messages8. Nikolay Yankov. Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomova Todor Stamatov *Mario Georgievao Nikolay Ivanovdo James GrahamStoyan TanevLukas Kovalik y... Ol:: AppsJira CloudToastAdd canvas@ Files/api/v2/u3 new messagests?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcalliзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message Serror = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNewNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Message Nikolay Yankov+...
|
58019
|
NULL
|
NULL
|
NULL
|
|
58024
|
2043
|
4
|
2026-05-19T11:28:14.835724+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190094835_m2.jpg...
|
Slack
|
! Nikolay Yankov (DM) - Jiminny Inc - 5 new items ! Nikolay Yankov (DM) - Jiminny Inc - 5 new items - Slack...
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormProiect vViewINavicarecodeKeractorTOOISWin PhostormProiect vViewINavicarecodeKeractorTOOISWindowFV faVsco.js°9 JY-20676-delete-report-related-objectsC ActivityController.ongphp api.php© AskAnythingController.php© AskAnythingPromptService.php© AutomatedReportsServiceTest.php› OpportunityMatcherOpportunitysyncstrategy© AskAnythingPromptDto.phppnp apl_vz.ong_ Prospectsearchstrategy© AskJiminnyReportsController.phg© AutomatedReportsService.php X C) AskAnythingPromptServiceTest.php© Search.phpW service Iraitsc) client.oneC DecorateAcuivity.onpclass AutomatedReportsserviceA100 ×3 M34 ^ V 199wDeleleobiecistrai.onoc)rielcDerinitions.onoC) PavloadBuillder.phpC) Profile.phpC) @uervBuilder.ohpC) @uervHandler.phpc) @uervlterator.ph17021734C) @uervResults.onoC) Service.ohrC) SvncBatchRedisService.oho1762N Traits© BaseClient.php© BaseService.php© CachedCrmServiceDecorator.php© CountryCodeResolver.phpC) CrmActivitvProviderIntearatedFventl isteoumacuvilyservice.ono© CrmConfigurationSettingsService.php1785© crmoo ectskesolver.ong© DefaultProspectSearchStrategy.php© cmallhelper.onp1780178717881818urinosProspecuintenace.ongc Lavoutmanacer.onoMacchbomainbycmallintenace.ono1822c) OpportunityActivityMatcher.pho1828- Opportunitysyncstrateavintertace.phoc) OpportunitysyncstrateayResolver.phoC) Prospectcache.php1836c) ProsoectSearchstrateav-actorv.ono18321 ProspectSearchStrategvinterface.ohoC) ProviderReaistrv.oho@ RecordSelector.ohnT ResolveComoanvNameBvEmailtrait.oho1848C) TimePerioditerator.oho>M imnorDinternalm Kioslv M AutomatedRenortsC) ActivitvTvneService nhn© AskJiminnyReportActivityService.php1856C) AutomatedRenortsCallbackService.nhC AutomatodDonorteCorvica nho© DealStagesService.php© RecipientsService.php13 usagespublic function getGenerateReportPayload(AutomatedReport $automatedReport, string $reportResultUui,esSinoutPavload - FE pavload structure7 usages188 0public function getActivitiesCountPayload(array $inputPayload): arravf.susagespublic function shouldSendReport(array $users, ?CarbonInterface $generatedAt = null): bool{...}5 usages-194public function hasPassedScheduledTime(?CarbonInterface $generatedAt, string $timezone): boolf...J 1951196b usagespublic function calculateFromAndToDatePeriod(=198string strequency?Carbon $fromDate = null,200rlarbon scouare = null.DateTimeZonelstring|null Stimezone = null: array {...}931 usageprivate function calculateFromAndToDate(AutomatedReport SautomatedReport): array{...}205public function aetAskJiminnvGenerateReportPavloaddAutomatedRenortResult SreportResult.211arrav Sactivitvids.•: array "...2132 usaded215private function getCallbackUrl: stringf...}* Validate and transform payload data for automated reports* @param array $data220221* @throws InvalidArgumentException* areturn arrayprivate function validateAndTransformData(array $data): array{...}"suppon Dally • In 32 m100% 5• Tue 19 May 14:28:14AskAnythingPromptServiceTest v=custom.log= laravel.log4 SF jiminny@localhost] x4 HS_local [jiminny@localhost]& console [PROD]# console [eu)CascadeA console [STAGING]C) CoachinaFeedhackCoachl.Icerin.nhnAsk Jiminny Report M+0 ..PlavaroundSo jiminny vMinor - onBeforeDelete only on on demandm 021 A1 A18 V2 Y6 ^SELECT ar.id, ar.uuid, ar.media type, ar.status, a.typeSp0M automated nenont neculte anJOIN automated_reports a ON a.id = ar.report idWHERE a.type = 'ask jiminny'LIMIT 10: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 = 275SELECT * FROM automated reports order by id desc›SELECT * FROMautomated_reportresults order by 1d desc:select * from activity searches where user id = 143:select * fronSELECTautomated renort results' * FROM 'automated renort resultsiINNER JOIN 'automated renortsiautomated renort results' "renort id' = 'automated renorts' 'idiautomated renort results' 'aenerated at' IS NOT NULLautomated renort results' 'sent at' IS NOT NULIautomated nenonts' "toam id' = 1SELECT * EPOM automated nenonte whene id = 47-SELECT * FROM automated_reports where id = 42;SELECT * FROM users WHERE id = 143; # group 28colost + fnom tonme whono id - 71/7.select * from crm_confiqurations where id = 500:select * from users where name = 'Integration Account': # 1695SELECT * FROMsocial accounts WHERE sociable_ id = 1695:select * from activities where crm_configuration id = 39and recording state = 'recorded' and duration > 60and status = 'completed' and actual start time >= 12025-12-01'•Not changed in this round - flagged for confirmation. Currently only on_demand prompts can be wired to Ask Jiminnyrepors (tne getaskyiminnykeportritters service expllciuy tercnes ASKAnychingPromptlarget::on_demand), so thetrontend quard is correctiv scoped. Worth a code comment. but no behavioral change needed.ФExplain @AskAnythingPrompt.php#L102-109 | need more infor about the count. Does this existAskomathing epors count on ssearen or prompt? Abut Dlo ntis not added as spromptbtost = her); dol need it andt atso what about editeHOW automated_reports_count gets on the modelLaravel's withCount() / loadCount() is the magic. When you call"phoSuser->searchesO->withcountautomatedReports')->qeto=Eloquent rewrites the SELECT to add a correlated subquerv:• sqSELECT activity searches.*.SELFCT GOUNT() FPOM autonated renorteWHERE automated reports.activity search id = activity searches..id) AS automated_reports_counFROM activity searches WHERE user id = ?The result is a single query. Each Search model comes back hydrated with an extra raw attribute called automated_reportscount (the relation name automatedReports is converted snake case+ count ). It lives in Smodel->getAttributes),Justiike id or nameloadCount() does the same thing but after the parent collection is already loaded - it runs one extra queryE salSELECT ask anything prompt id, COUNT(*) AS aggregateView allSELECT * FROM activities WHERE uuid to bin('458cf915-6914-4000-b083-568763262956') = uuid:B AskAnythingPromptServiceTest.php +25-2app/Component/AskAnything/@ AskAnythingPromptService.php +7 -2app/Repositories/ D AskAnythingRepository.php +7 -1ann/Httn/Trancformorc/MSoarchTrancformornhntselect * from leadsilers/API/ ActivityController.php +1/m AskAnvthinaPromot.oho +4Reiect alllAccent alliSELECT * FROM activities WHERE uuid to bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid: # 422003SFLECT + EPOM activitiec WHEPE id TN (16. 422003).Ask anvthina 884-L)<> Code Claude Qnus 1.7 MediumSocket fail to connect to host:address=(host=localhost) (port=3306)(tvpe=primarv). Connection refusedWN Windsurf Teams1787-1io 4 spaces...
|
NULL
|
-4820073085019832826
|
NULL
|
click
|
ocr
|
NULL
|
PhostormProiect vViewINavicarecodeKeractorTOOISWin PhostormProiect vViewINavicarecodeKeractorTOOISWindowFV faVsco.js°9 JY-20676-delete-report-related-objectsC ActivityController.ongphp api.php© AskAnythingController.php© AskAnythingPromptService.php© AutomatedReportsServiceTest.php› OpportunityMatcherOpportunitysyncstrategy© AskAnythingPromptDto.phppnp apl_vz.ong_ Prospectsearchstrategy© AskJiminnyReportsController.phg© AutomatedReportsService.php X C) AskAnythingPromptServiceTest.php© Search.phpW service Iraitsc) client.oneC DecorateAcuivity.onpclass AutomatedReportsserviceA100 ×3 M34 ^ V 199wDeleleobiecistrai.onoc)rielcDerinitions.onoC) PavloadBuillder.phpC) Profile.phpC) @uervBuilder.ohpC) @uervHandler.phpc) @uervlterator.ph17021734C) @uervResults.onoC) Service.ohrC) SvncBatchRedisService.oho1762N Traits© BaseClient.php© BaseService.php© CachedCrmServiceDecorator.php© CountryCodeResolver.phpC) CrmActivitvProviderIntearatedFventl isteoumacuvilyservice.ono© CrmConfigurationSettingsService.php1785© crmoo ectskesolver.ong© DefaultProspectSearchStrategy.php© cmallhelper.onp1780178717881818urinosProspecuintenace.ongc Lavoutmanacer.onoMacchbomainbycmallintenace.ono1822c) OpportunityActivityMatcher.pho1828- Opportunitysyncstrateavintertace.phoc) OpportunitysyncstrateayResolver.phoC) Prospectcache.php1836c) ProsoectSearchstrateav-actorv.ono18321 ProspectSearchStrategvinterface.ohoC) ProviderReaistrv.oho@ RecordSelector.ohnT ResolveComoanvNameBvEmailtrait.oho1848C) TimePerioditerator.oho>M imnorDinternalm Kioslv M AutomatedRenortsC) ActivitvTvneService nhn© AskJiminnyReportActivityService.php1856C) AutomatedRenortsCallbackService.nhC AutomatodDonorteCorvica nho© DealStagesService.php© RecipientsService.php13 usagespublic function getGenerateReportPayload(AutomatedReport $automatedReport, string $reportResultUui,esSinoutPavload - FE pavload structure7 usages188 0public function getActivitiesCountPayload(array $inputPayload): arravf.susagespublic function shouldSendReport(array $users, ?CarbonInterface $generatedAt = null): bool{...}5 usages-194public function hasPassedScheduledTime(?CarbonInterface $generatedAt, string $timezone): boolf...J 1951196b usagespublic function calculateFromAndToDatePeriod(=198string strequency?Carbon $fromDate = null,200rlarbon scouare = null.DateTimeZonelstring|null Stimezone = null: array {...}931 usageprivate function calculateFromAndToDate(AutomatedReport SautomatedReport): array{...}205public function aetAskJiminnvGenerateReportPavloaddAutomatedRenortResult SreportResult.211arrav Sactivitvids.•: array "...2132 usaded215private function getCallbackUrl: stringf...}* Validate and transform payload data for automated reports* @param array $data220221* @throws InvalidArgumentException* areturn arrayprivate function validateAndTransformData(array $data): array{...}"suppon Dally • In 32 m100% 5• Tue 19 May 14:28:14AskAnythingPromptServiceTest v=custom.log= laravel.log4 SF jiminny@localhost] x4 HS_local [jiminny@localhost]& console [PROD]# console [eu)CascadeA console [STAGING]C) CoachinaFeedhackCoachl.Icerin.nhnAsk Jiminny Report M+0 ..PlavaroundSo jiminny vMinor - onBeforeDelete only on on demandm 021 A1 A18 V2 Y6 ^SELECT ar.id, ar.uuid, ar.media type, ar.status, a.typeSp0M automated nenont neculte anJOIN automated_reports a ON a.id = ar.report idWHERE a.type = 'ask jiminny'LIMIT 10: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 = 275SELECT * FROM automated reports order by id desc›SELECT * FROMautomated_reportresults order by 1d desc:select * from activity searches where user id = 143:select * fronSELECTautomated renort results' * FROM 'automated renort resultsiINNER JOIN 'automated renortsiautomated renort results' "renort id' = 'automated renorts' 'idiautomated renort results' 'aenerated at' IS NOT NULLautomated renort results' 'sent at' IS NOT NULIautomated nenonts' "toam id' = 1SELECT * EPOM automated nenonte whene id = 47-SELECT * FROM automated_reports where id = 42;SELECT * FROM users WHERE id = 143; # group 28colost + fnom tonme whono id - 71/7.select * from crm_confiqurations where id = 500:select * from users where name = 'Integration Account': # 1695SELECT * FROMsocial accounts WHERE sociable_ id = 1695:select * from activities where crm_configuration id = 39and recording state = 'recorded' and duration > 60and status = 'completed' and actual start time >= 12025-12-01'•Not changed in this round - flagged for confirmation. Currently only on_demand prompts can be wired to Ask Jiminnyrepors (tne getaskyiminnykeportritters service expllciuy tercnes ASKAnychingPromptlarget::on_demand), so thetrontend quard is correctiv scoped. Worth a code comment. but no behavioral change needed.ФExplain @AskAnythingPrompt.php#L102-109 | need more infor about the count. Does this existAskomathing epors count on ssearen or prompt? Abut Dlo ntis not added as spromptbtost = her); dol need it andt atso what about editeHOW automated_reports_count gets on the modelLaravel's withCount() / loadCount() is the magic. When you call"phoSuser->searchesO->withcountautomatedReports')->qeto=Eloquent rewrites the SELECT to add a correlated subquerv:• sqSELECT activity searches.*.SELFCT GOUNT() FPOM autonated renorteWHERE automated reports.activity search id = activity searches..id) AS automated_reports_counFROM activity searches WHERE user id = ?The result is a single query. Each Search model comes back hydrated with an extra raw attribute called automated_reportscount (the relation name automatedReports is converted snake case+ count ). It lives in Smodel->getAttributes),Justiike id or nameloadCount() does the same thing but after the parent collection is already loaded - it runs one extra queryE salSELECT ask anything prompt id, COUNT(*) AS aggregateView allSELECT * FROM activities WHERE uuid to bin('458cf915-6914-4000-b083-568763262956') = uuid:B AskAnythingPromptServiceTest.php +25-2app/Component/AskAnything/@ AskAnythingPromptService.php +7 -2app/Repositories/ D AskAnythingRepository.php +7 -1ann/Httn/Trancformorc/MSoarchTrancformornhntselect * from leadsilers/API/ ActivityController.php +1/m AskAnvthinaPromot.oho +4Reiect alllAccent alliSELECT * FROM activities WHERE uuid to bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid: # 422003SFLECT + EPOM activitiec WHEPE id TN (16. 422003).Ask anvthina 884-L)<> Code Claude Qnus 1.7 MediumSocket fail to connect to host:address=(host=localhost) (port=3306)(tvpe=primarv). Connection refusedWN Windsurf Teams1787-1io 4 spaces...
|
57993
|
NULL
|
NULL
|
NULL
|
|
58025
|
2042
|
5
|
2026-05-19T11:28:18.548119+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190098548_m1.jpg...
|
Firefox
|
Work — Mozilla Firefox
|
1
|
github.com/jiminny/app/pull/12098
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfilesToolsWi FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp• 0APPDOCKER• ₴1DEV (docker)₴82APP (-zslreturn new JsonResponse(['error'Serror],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLater..•More>0.(ahlSupport Daily • in 32 m100% <78• Tue 19 May 14:28:18ED→QDescribe what you are looking forJiminny …..scnicrat# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi.... Direct messages8. Nikolay Yankov% Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomova Todor Stamatov *Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...l:: AppsJira CloudToastNikolay Yankov6 0• Messages+Add canvas@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNewNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Message Nikolay Yankov+...
|
NULL
|
6667865021922530185
|
NULL
|
click
|
ocr
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfilesToolsWi FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp• 0APPDOCKER• ₴1DEV (docker)₴82APP (-zslreturn new JsonResponse(['error'Serror],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLater..•More>0.(ahlSupport Daily • in 32 m100% <78• Tue 19 May 14:28:18ED→QDescribe what you are looking forJiminny …..scnicrat# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi.... Direct messages8. Nikolay Yankov% Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomova Todor Stamatov *Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...l:: AppsJira CloudToastNikolay Yankov6 0• Messages+Add canvas@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNewNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Message Nikolay Yankov+...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58027
|
2043
|
5
|
2026-05-19T11:28:24.125805+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190104125_m2.jpg...
|
Firefox
|
JY-20676 delete AJ reports related objects by Laky JY-20676 delete AJ reports related objects by LakyLak · Pull Request #12098 · jiminny/app — Work...
|
1
|
github.com/jiminny/app/pull/12098
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Firefox File Edit View History Bookmarks Profile Firefox File Edit View History Bookmarks Profiles Tools Window Help.0+→x© github.com/jiminny/app/pull/12098i Platform Sprint 4 Q2 - Platform Te:Proiect Phoenix — Fiamal® On April 24 we'll start using GitHub Copilot interaction data for Al model training unless you opt out. Review this update and manage your preferences in your GitHub account settings.Service-Desk - Queues - Platform• JY-20725 add HS rate limit handlitJy-20808 low priority indexing quc© Pipelines - jiminny/app#) New Tal8 Jiminny) SevenShores|Hubspot|Exceptions)( JY-20676 delete AJ reports relate8 Jiminny•JY-20676 delete A.J reports rel X+ New TabJY-20676 delete AJ reports related objects #12098 •LakyLak wants to merge 6 commits into master from JY-20676-delete-report-related-objects(12 Open# Conversation- Commits 6 E Checks E Files changedLakyLak commented z hours ago • edited by nikolay-vankov ~JIRA: JY-20676Changes:• Add field has_reports to Activity search and Ask Jiminny prompts lists• mark related obiect nuli on its deletion and disable revort I• Validate prompt and search tields on activate report toaaleSettings(+) Add nev:: + shighj#:+ test2This prompt is used in reports"test 2" is used in one or more Ask Jiminny reports.Deleting it will disable those reports.If you want to keep them running, edit the reportsto use a alrerent prompttirst..Trying to enable a report that has a deleted prompt:C< 40 lbl Ol SupportDaily•in 32m A 100% C3 • Tue 19 May 14:28:23Code -ReviewersLoading suggestions..Still in proaress? Convert to draftAssigneesNo one-assign yourselfLabelsNone yetProjectsNone veDevelopmentSuccessfullv mergina this pull reguest may close thes!NotificationsUnsubscribeYou're receivina notifications because vou're watchinalthis repository.2 participants• Lock conversationnt.com/36303598/594640569-fe44c5d4-d15a-40d8-a061-70f486e0816e.pna?iwteevJ0eXAioiJKV10iLCJhbGcioiJtl50ZW50L...YiNhYTKOMTVKMGU1OGI2MDY2YTMOYTc1MmVkM2VmZmU5OWNmZiZkMWEmZTM4NTc5ZDU3MG.JIMzZ/ZDMvMicmWC1BbXotU2InbmVkSGVhZGVvcz1ob3N0.JnJlc3BvbnNILWNvbnRIbnOtdHlwZT1obWEnZSUvRnBuZvJ9.Polk7tMavteVxHMrk33aLViaEDT_I0224Xo05Ma6vOw...
|
NULL
|
7779152709599453855
|
NULL
|
click
|
ocr
|
NULL
|
Firefox File Edit View History Bookmarks Profile Firefox File Edit View History Bookmarks Profiles Tools Window Help.0+→x© github.com/jiminny/app/pull/12098i Platform Sprint 4 Q2 - Platform Te:Proiect Phoenix — Fiamal® On April 24 we'll start using GitHub Copilot interaction data for Al model training unless you opt out. Review this update and manage your preferences in your GitHub account settings.Service-Desk - Queues - Platform• JY-20725 add HS rate limit handlitJy-20808 low priority indexing quc© Pipelines - jiminny/app#) New Tal8 Jiminny) SevenShores|Hubspot|Exceptions)( JY-20676 delete AJ reports relate8 Jiminny•JY-20676 delete A.J reports rel X+ New TabJY-20676 delete AJ reports related objects #12098 •LakyLak wants to merge 6 commits into master from JY-20676-delete-report-related-objects(12 Open# Conversation- Commits 6 E Checks E Files changedLakyLak commented z hours ago • edited by nikolay-vankov ~JIRA: JY-20676Changes:• Add field has_reports to Activity search and Ask Jiminny prompts lists• mark related obiect nuli on its deletion and disable revort I• Validate prompt and search tields on activate report toaaleSettings(+) Add nev:: + shighj#:+ test2This prompt is used in reports"test 2" is used in one or more Ask Jiminny reports.Deleting it will disable those reports.If you want to keep them running, edit the reportsto use a alrerent prompttirst..Trying to enable a report that has a deleted prompt:C< 40 lbl Ol SupportDaily•in 32m A 100% C3 • Tue 19 May 14:28:23Code -ReviewersLoading suggestions..Still in proaress? Convert to draftAssigneesNo one-assign yourselfLabelsNone yetProjectsNone veDevelopmentSuccessfullv mergina this pull reguest may close thes!NotificationsUnsubscribeYou're receivina notifications because vou're watchinalthis repository.2 participants• Lock conversationnt.com/36303598/594640569-fe44c5d4-d15a-40d8-a061-70f486e0816e.pna?iwteevJ0eXAioiJKV10iLCJhbGcioiJtl50ZW50L...YiNhYTKOMTVKMGU1OGI2MDY2YTMOYTc1MmVkM2VmZmU5OWNmZiZkMWEmZTM4NTc5ZDU3MG.JIMzZ/ZDMvMicmWC1BbXotU2InbmVkSGVhZGVvcz1ob3N0.JnJlc3BvbnNILWNvbnRIbnOtdHlwZT1obWEnZSUvRnBuZvJ9.Polk7tMavteVxHMrk33aLViaEDT_I0224Xo05Ma6vOw...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58026
|
2042
|
6
|
2026-05-19T11:28:26.998712+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190106998_m1.jpg...
|
Firefox
|
594640569-fe44c5d4-d15a-40d8-a061-70f486e0816e.png 594640569-fe44c5d4-d15a-40d8-a061-70f486e0816e.png (PNG Image, 1996 × 1656 pixels) — Scaled (71%) — Work...
|
1
|
private-user-images.githubusercontent.com/36303598 private-user-images.githubusercontent.com/36303598/594640569-fe44c5d4-d15a-40d8-a061-70f486e0816e.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzkxOTAzOTksIm5iZiI6MTc3OTE5MDA5OSwicGF0aCI6Ii8zNjMwMzU5OC81OTQ2NDA1NjktZmU0NGM1ZDQtZDE1YS00MGQ4LWEwNjEtNzBmNDg2ZTA4MTZlLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNjA1MTklMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjYwNTE5VDExMjgxOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdlYjNhYTk0MTVkMGU1OGI2MDYzYTM0YTc1MmVkM2VmZmU5OWNmZjZkMWFmZTM4NTc5ZDU3MGJlMzZjZDMyYjcmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JnJlc3BvbnNlLWNvbnRlbnQtdHlwZT1pbWFnZSUyRnBuZyJ9.PoIk7tMayteVxHMrk33gLViaFDT_lo224Xp05Ma6y0w...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
594640569-fe44c5d4-d15a-40d8-a061-70f486e0816e.png 594640569-fe44c5d4-d15a-40d8-a061-70f486e0816e.png (PNG Image, 1996 × 1656 pixels) — Scaled (71%)
private-user-images.githubusercontent.com...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"594640569-fe44c5d4-d15a-40d8-a061-70f486e0816e.png (PNG Image, 1996 × 1656 pixels) — Scaled (71%)","depth":4,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"private-user-images.githubusercontent.com","depth":4,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-2327636473172052055
|
-5719998149881605388
|
click
|
accessibility
|
NULL
|
594640569-fe44c5d4-d15a-40d8-a061-70f486e0816e.png 594640569-fe44c5d4-d15a-40d8-a061-70f486e0816e.png (PNG Image, 1996 × 1656 pixels) — Scaled (71%)
private-user-images.githubusercontent.com...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58028
|
2043
|
6
|
2026-05-19T11:28:31.336489+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190111336_m2.jpg...
|
Firefox
|
JY-20676 delete AJ reports related objects by Laky JY-20676 delete AJ reports related objects by LakyLak · Pull Request #12098 · jiminny/app — Work...
|
1
|
github.com/jiminny/app/pull/12098
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Platform Sprint 4 Q2 - Platform Team - Scrum Board Platform Sprint 4 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 4 Q2 - Platform Team - Scrum Board - Jira
Project Phoenix – Figma
Project Phoenix – Figma
Service-Desk - Queues - Platform team - Service space - Jira
Service-Desk - Queues - Platform team - Service space - Jira
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20808 low priority indexing queue by Vasil-Jiminny · Pull Request #12082 · jiminny/app
JY-20808 low priority indexing queue by Vasil-Jiminny · Pull Request #12082 · jiminny/app
Pipelines - jiminny/app
Pipelines - jiminny/app
New Tab
New Tab
Jiminny
Jiminny
SevenShores\Hubspot\Exceptions\BadRequest: 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
SevenShores\Hubspot\Exceptions\BadRequest: 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
JY-20676 delete AJ reports related objects by LakyLak · Pull Request #12098 · jiminny/app
JY-20676 delete AJ reports related objects by LakyLak · Pull Request #12098 · jiminny/app
Jiminny
Jiminny
JY-20676 delete AJ reports related objects by LakyLak · Pull Request #12098 · jiminny/app
JY-20676 delete AJ reports related objects by LakyLak · Pull Request #12098 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
All issues(g then i)
All pull requests
All repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (32)
Pull requests
(
32
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality
Security and quality
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
....
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Platform Sprint 4 Q2 - Platform Team - Scrum Board - Jira","depth":4,"bounds":{"left":0.0,"top":0.0518755,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Platform Sprint 4 Q2 - Platform Team - Scrum Board - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.06304868,"width":0.10106383,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Project Phoenix – Figma","depth":4,"bounds":{"left":0.0,"top":0.08459697,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Project Phoenix – Figma","depth":5,"bounds":{"left":0.013297873,"top":0.09577015,"width":0.041888297,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Service-Desk - Queues - Platform team - Service space - Jira","depth":4,"bounds":{"left":0.0,"top":0.11731844,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Service-Desk - Queues - Platform team - Service space - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.12849163,"width":0.10721409,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.15003991,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.16121309,"width":0.18816489,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20808 low priority indexing queue by Vasil-Jiminny · Pull Request #12082 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.18276137,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20808 low priority indexing queue by Vasil-Jiminny · Pull Request #12082 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.19393456,"width":0.15674867,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pipelines - jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.21548285,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pipelines - jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.22665602,"width":0.039228722,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"New Tab","depth":4,"bounds":{"left":0.0,"top":0.2482043,"width":0.07962101,"height":0.032721467},"on_screen":true,"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.013297873,"top":0.25937748,"width":0.014960106,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny","depth":4,"bounds":{"left":0.0,"top":0.28092578,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny","depth":5,"bounds":{"left":0.013297873,"top":0.29209897,"width":0.013131649,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SevenShores\\Hubspot\\Exceptions\\BadRequest: 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","depth":4,"bounds":{"left":0.0,"top":0.31364724,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SevenShores\\Hubspot\\Exceptions\\BadRequest: 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","depth":5,"bounds":{"left":0.013297873,"top":0.32482043,"width":0.4644282,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20676 delete AJ reports related objects by LakyLak · Pull Request #12098 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.3463687,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20676 delete AJ reports related objects by LakyLak · Pull Request #12098 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.3575419,"width":0.15791224,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny","depth":4,"bounds":{"left":0.0,"top":0.3790902,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny","depth":5,"bounds":{"left":0.013297873,"top":0.39026338,"width":0.013131649,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20676 delete AJ reports related objects by LakyLak · Pull Request #12098 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.41181165,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"JY-20676 delete AJ reports related objects by LakyLak · Pull Request #12098 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.42298484,"width":0.15791224,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.06732048,"top":0.41899443,"width":0.007978723,"height":0.01915403},"on_screen":true,"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.0028257978,"top":0.4461293,"width":0.07413564,"height":0.025538707},"on_screen":true,"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.0028257978,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"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.013796543,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"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.024933511,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"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.036070477,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"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.04720745,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Skip to content","depth":6,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Skip to content","depth":7,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Open menu","depth":10,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Homepage (g then d)","depth":9,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"jiminny","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"jiminny","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"app","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"app","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Search or jump to…","depth":9,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Type","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to search","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Chat with Copilot","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"Open Copilot…","depth":9,"on_screen":false,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Create new...","depth":9,"on_screen":false,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"All issues(g then i)","depth":9,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"All pull requests","depth":9,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"All repositories","depth":9,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"You have unread notifications(g then n)","depth":9,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open user navigation menu","depth":9,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Repository navigation","depth":9,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Repository navigation","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Code","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Code","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Pull requests (32)","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"32","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Agents","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Agents","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Actions","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Actions","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Wiki","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Wiki","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Security and quality","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Security and quality","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Insights","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Insights","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Settings","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Settings","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Important update","depth":10,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Important update","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Review this update","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Review this update","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and manage your preferences in your","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"GitHub account settings","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"GitHub account settings","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
8399541510089785320
|
-988971431693102963
|
visual_change
|
accessibility
|
NULL
|
Platform Sprint 4 Q2 - Platform Team - Scrum Board Platform Sprint 4 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 4 Q2 - Platform Team - Scrum Board - Jira
Project Phoenix – Figma
Project Phoenix – Figma
Service-Desk - Queues - Platform team - Service space - Jira
Service-Desk - Queues - Platform team - Service space - Jira
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20808 low priority indexing queue by Vasil-Jiminny · Pull Request #12082 · jiminny/app
JY-20808 low priority indexing queue by Vasil-Jiminny · Pull Request #12082 · jiminny/app
Pipelines - jiminny/app
Pipelines - jiminny/app
New Tab
New Tab
Jiminny
Jiminny
SevenShores\Hubspot\Exceptions\BadRequest: 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
SevenShores\Hubspot\Exceptions\BadRequest: 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
JY-20676 delete AJ reports related objects by LakyLak · Pull Request #12098 · jiminny/app
JY-20676 delete AJ reports related objects by LakyLak · Pull Request #12098 · jiminny/app
Jiminny
Jiminny
JY-20676 delete AJ reports related objects by LakyLak · Pull Request #12098 · jiminny/app
JY-20676 delete AJ reports related objects by LakyLak · Pull Request #12098 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
All issues(g then i)
All pull requests
All repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (32)
Pull requests
(
32
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality
Security and quality
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
....
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58029
|
2042
|
7
|
2026-05-19T11:28:36.438937+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190116438_m1.jpg...
|
Firefox
|
DXP4800PLUS-B5F8 — Personal
|
1
|
nas.lakylak.xyz/desktop/#/
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
Close tab
Gettin DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
Close tab
Getting Started · AFFiNE
Getting Started · AFFiNE
Screenpipe — Archive
Screenpipe — Archive
Download screenpipe — get started in minutes
Download screenpipe — get started in minutes
Self-Hosted Software and Apps...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"DXP4800PLUS-B5F8","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"DXP4800PLUS-B5F8","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"Getting Started · AFFiNE","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Getting Started · AFFiNE","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Screenpipe — Archive","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Screenpipe — Archive","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Download screenpipe — get started in minutes","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Download screenpipe — get started in minutes","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Self-Hosted Software and Apps","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
996315124897554338
|
-4591336106878719341
|
app_switch
|
accessibility
|
NULL
|
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
Close tab
Gettin DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
Close tab
Getting Started · AFFiNE
Getting Started · AFFiNE
Screenpipe — Archive
Screenpipe — Archive
Download screenpipe — get started in minutes
Download screenpipe — get started in minutes
Self-Hosted Software and Apps...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58030
|
2043
|
7
|
2026-05-19T11:28:37.404895+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190117404_m2.jpg...
|
Slack
|
Nikolay Yankov (DM) - Jiminny Inc - 5 new items - Nikolay Yankov (DM) - Jiminny Inc - 5 new items - Slack...
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rirero)rlleEditVIewhistory bookmarksProtllesToolsW rirero)rlleEditVIewhistory bookmarksProtllesToolsWindowmelp•••D+→с• github.com/jinPlatform Sprint 4 Q2 - Platform TeProiect Phoenix - Fiama I1 Service-Desk - Queues - PlatformJY-20725 add HS rate limit handlitJY-20808 low priority indexing que© Pipelines - jiminny/appNew Tab8 JiminnyA SevenShores|Hubspot|ExceptionsJY-20676 delete AJ reports relate8 JiminnyJy-20676 delete A.J reports rel X+ New Tab83 openJY-20676 delete AJ reports related objects #12098LakyLak wants to merge 6 commits into master trom JY-206/6-0Irvina to enaole a revorr tnar nas a celetec oromoiO1 Screen.Recording.2026-05-19.at.14.01.22.movASk Jiminny KeportsO Clear allFREQUENCY -$IDaily) DXP4800PLUS-B5F8Getting Started - AFFiNE® Screenpipe - ArchiveDownload screenpipe - get•• Self-Hosted Software and Ar* New Tab•и Помогнете ни ла развиваїЗавеждане на щета онлай,# Kontakt | Velvvslanectvo SidNginx Proxy ManagerA Location Logge:. Providers - Admin - authent3.B. Hotel Bellisimo, Lozenets (UF)[ Електронно банкиране ДС!+ New TabTait the repormts compiate d e setup borfrormpe@Jy-20676 delete AJ reports related obiectslNew changes since you last viewed• - nas.lakylak.xyz/desktop/#/FilesControl PanelsioradeCloud DrivesFile Version ExplorerOverviewStorageHard Drive• External StorageLOOSTask ManagerMusicDUNASync & Backuplohl"suppon Dally • In 32m100% Lz&• Tue 19 May 14:28:37UGREEN AIE3 Control Paneluil StorageStorage pool & VolumeData organizingAdvanced settingsSlorage Pool 1 Descriotion &Rebuildina.RAID 5 Hard Drive 1. 2. 3. 4 10.8T8|Current progress: 38.3% Estimated time remaining: 1 days 6 h10.8TRStorage Pool 2 Description&Basic M.2 Hard Drive 2 916.1G&Volume 2 Description &Normal Btrfs 916.1GBUsed: 219.9GB...
|
NULL
|
4007027514344429696
|
NULL
|
visual_change
|
ocr
|
NULL
|
rirero)rlleEditVIewhistory bookmarksProtllesToolsW rirero)rlleEditVIewhistory bookmarksProtllesToolsWindowmelp•••D+→с• github.com/jinPlatform Sprint 4 Q2 - Platform TeProiect Phoenix - Fiama I1 Service-Desk - Queues - PlatformJY-20725 add HS rate limit handlitJY-20808 low priority indexing que© Pipelines - jiminny/appNew Tab8 JiminnyA SevenShores|Hubspot|ExceptionsJY-20676 delete AJ reports relate8 JiminnyJy-20676 delete A.J reports rel X+ New Tab83 openJY-20676 delete AJ reports related objects #12098LakyLak wants to merge 6 commits into master trom JY-206/6-0Irvina to enaole a revorr tnar nas a celetec oromoiO1 Screen.Recording.2026-05-19.at.14.01.22.movASk Jiminny KeportsO Clear allFREQUENCY -$IDaily) DXP4800PLUS-B5F8Getting Started - AFFiNE® Screenpipe - ArchiveDownload screenpipe - get•• Self-Hosted Software and Ar* New Tab•и Помогнете ни ла развиваїЗавеждане на щета онлай,# Kontakt | Velvvslanectvo SidNginx Proxy ManagerA Location Logge:. Providers - Admin - authent3.B. Hotel Bellisimo, Lozenets (UF)[ Електронно банкиране ДС!+ New TabTait the repormts compiate d e setup borfrormpe@Jy-20676 delete AJ reports related obiectslNew changes since you last viewed• - nas.lakylak.xyz/desktop/#/FilesControl PanelsioradeCloud DrivesFile Version ExplorerOverviewStorageHard Drive• External StorageLOOSTask ManagerMusicDUNASync & Backuplohl"suppon Dally • In 32m100% Lz&• Tue 19 May 14:28:37UGREEN AIE3 Control Paneluil StorageStorage pool & VolumeData organizingAdvanced settingsSlorage Pool 1 Descriotion &Rebuildina.RAID 5 Hard Drive 1. 2. 3. 4 10.8T8|Current progress: 38.3% Estimated time remaining: 1 days 6 h10.8TRStorage Pool 2 Description&Basic M.2 Hard Drive 2 916.1G&Volume 2 Description &Normal Btrfs 916.1GBUsed: 219.9GB...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58032
|
2042
|
8
|
2026-05-19T11:28:37.535579+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190117535_m1.jpg...
|
Slack
|
Nikolay Yankov (DM) - Jiminny Inc - 5 new items - Nikolay Yankov (DM) - Jiminny Inc - 5 new items - Slack...
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfilesToolsWi FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp• 0APPDOCKER• ₴1DEV (docker)₴82APP (-zslreturn new JsonResponse(['error'$error],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:remote:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.remote:Total 158(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php>0.(ahlSupport Daily • in 32 m100% <7Tue 19 May 14:28:37•••EDQHomeDMsActivityFilesLater...More→Describe what you are looking forJiminny …..scnicrat# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi.... Direct messages8. Nikolay YankovD. Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomovg Todor Stamatov "Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...l:: AppsJira CloudToastNikolay Yankov6 0• Messages+Add canvas@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNewNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Message Nikolay Yankov+...
|
NULL
|
3564244974693171386
|
NULL
|
app_switch
|
ocr
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfilesToolsWi FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp• 0APPDOCKER• ₴1DEV (docker)₴82APP (-zslreturn new JsonResponse(['error'$error],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:remote:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.remote:Total 158(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php>0.(ahlSupport Daily • in 32 m100% <7Tue 19 May 14:28:37•••EDQHomeDMsActivityFilesLater...More→Describe what you are looking forJiminny …..scnicrat# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi.... Direct messages8. Nikolay YankovD. Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomovg Todor Stamatov "Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...l:: AppsJira CloudToastNikolay Yankov6 0• Messages+Add canvas@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNewNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Message Nikolay Yankov+...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58031
|
2043
|
8
|
2026-05-19T11:28:39.908153+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190119908_m2.jpg...
|
Firefox
|
JY-20676 delete AJ reports related objects by Laky JY-20676 delete AJ reports related objects by LakyLak · Pull Request #12098 · jiminny/app — Work...
|
1
|
github.com/jiminny/app/pull/12098
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Platform Sprint 4 Q2 - Platform Team - Scrum Board Platform Sprint 4 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 4 Q2 - Platform Team - Scrum Board - Jira
Project Phoenix – Figma...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Platform Sprint 4 Q2 - Platform Team - Scrum Board - Jira","depth":4,"bounds":{"left":0.0,"top":0.0518755,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Platform Sprint 4 Q2 - Platform Team - Scrum Board - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.06304868,"width":0.10106383,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Project Phoenix – Figma","depth":4,"bounds":{"left":0.0,"top":0.08459697,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
-721241671567945238
|
7230047910636552213
|
app_switch
|
accessibility
|
NULL
|
Platform Sprint 4 Q2 - Platform Team - Scrum Board Platform Sprint 4 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 4 Q2 - Platform Team - Scrum Board - Jira
Project Phoenix – Figma...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58033
|
2043
|
9
|
2026-05-19T11:29:14.436507+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190154436_m2.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskAnythingPromptServiceTest
Run 'AskAnythingPromptServiceTest'
Debug 'AskAnythingPromptServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
100
3
34
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Kiosk\AutomatedReports;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Carbon\Exceptions\InvalidFormatException;
use DateTime;
use DateTimeInterface;
use DateTimeZone;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\ActivitySearch\FilterDefinition\InputTypeEnum;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Partner;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Throwable;
class AutomatedReportsService
{
public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';
public const string TYPE_ASK_JIMINNY = 'ask_jiminny';
/**
* Standard report types (used by kiosk for existing automated reports).
*/
// @TODO this will add filter, however if we need to control feature by FF we need conditional logic
public const array TYPES = [
['id' => 'exec_summary', 'name' => 'Exec Summary'],
['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],
['id' => 'product_feedback', 'name' => 'Product Feedback'],
['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],
// ['id' => 'questions', 'name' => 'Questions'],
// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],
];
public const array ALL_TYPES = [
...self::TYPES,
['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],
];
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';
/**
* Frequencies for standard (non-Ask Jiminny) reports.
*/
public const array FREQUENCIES = [
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
/**
* Frequencies for Ask Jiminny reports.
*/
public const array ASK_JIMINNY_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
];
public const string MEDIA_TYPE_PDF = 'pdf';
public const string MEDIA_TYPE_PODCAST = 'podcast';
public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];
public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];
public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];
public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];
public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];
public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];
public const int SENT_REPORT_AT_HOURS = 5;
public const string PDF_KEY = 'pdf';
public const string AUDIO_KEY = 'audio';
private const array ALL_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
private const string S3_DIR = 'reports';
private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];
private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly GroupRepository $groupRepository,
private readonly UserRepository $userRepository,
private readonly StageRepository $stageRepository,
private readonly DealStagesService $dealStagesService,
private readonly RecipientsService $recipientsService,
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly Webhook $webhookService,
private readonly BusDispatcher $dispatcher,
private readonly ActivityTypeService $activityTypeService,
private readonly PlaybookCategoryRepository $playbookCategoryRepository,
private readonly AskAnythingPromptService $askAnythingPromptService,
private readonly SearchRepository $activitySearchRepository,
private readonly AskAnythingRepository $askAnythingRepository,
) {
}
public static function getTypes(): array
{
$types = self::TYPES;
return array_map(static function ($type) {
return $type['id'];
}, $types);
}
public static function getCallTypes(): array
{
return array_map(static function ($callType) {
return $callType['id'];
}, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);
}
public static function getFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::FREQUENCIES);
}
// front-facing structure
public function getReportEnabledFieldData(bool $value = false): array
{
return [
'id' => 'report_enabled',
'label' => '',
'inputType' => InputTypeEnum::TOGGLE,
'value' => $value,
];
}
// Organizations = Teams
public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array
{
$options = $this->getTeams(partner: $partner);
if ($shortVersion) {
return [
'id' => 'organization',
'label' => 'Organization',
'options' => $options,
];
}
return [
'id' => 'organization',
'label' => 'Organization',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $options,
'value' => $value,
'dependencies' => [
'teams',
'deal_stage_at_call',
'current_deal_stage',
'recipients',
ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,
],
'dependsOn' => [],
];
}
// Teams = Groups
public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array
{
if ($shortVersion) {
return [
'id' => 'teams',
'label' => 'Team',
'options' => $options,
];
}
return [
'id' => 'teams',
'label' => 'Team',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => false,
'placeholder' => 'Select',
'options' => $options,
'value' => $value, // value should be an array of objects {id, name}
'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],
'dependsOn' => [],
];
}
public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array
{
$types = [];
if ($team instanceof Team) {
if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
$types = self::TYPES;
}
if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {
$types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];
}
} else {
$types = self::TYPES;
}
if ($shortVersion) {
return [
'id' => 'report_type',
'label' => 'Report Type',
'options' => $types,
];
}
return [
'id' => 'report_type',
'label' => 'Report Type',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $types,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getFrequencyFieldData(?string $value = null): array
{
return [
'id' => 'frequency',
'label' => 'Frequency',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => self::FREQUENCIES,
'value' => $value,
'dependencies' => ['period'],
'dependsOn' => [],
];
}
public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array
{
return [
'id' => 'period',
'label' => 'Select one-off period',
'inputType' => InputTypeEnum::DATE_RANGE,
'required' => true,
'placeholder' => 'Select',
'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],
'queryParams' => [
'startDate' => 'start_date_period',
'endDate' => 'end_date_period',
],
'dependencies' => [],
'dependsOn' => ['frequency'],
];
}
public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array
{
return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);
}
public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);
}
public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);
}
public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'deal_value',
'label' => 'Deal Value',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_deal_value',
'max' => 'max_deal_value',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array
{
$value = [];
$conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;
$dialerOn && $value[] = self::CALL_TYPE_DIALER;
return [
'id' => 'call_type',
'label' => 'Call Type',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => [
self::CALL_TYPE_CONFERENCE,
self::CALL_TYPE_DIALER,
],
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getMediaTypeFieldData(?AutomatedReport $report = null): array
{
$value = [];
if ($report) {
$value = $this->transformMediaTypes($report);
}
return [
'id' => 'media_types',
'label' => 'Export as',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => self::MEDIA_TYPE_OBJECTS,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'call_duration',
'label' => 'Call Duration',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_call_duration',
'max' => 'max_call_duration',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getRecipientsFieldData(?Team $team = null, array $value = []): array
{
return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);
}
public function getJiminnyRecipientsFieldData(array $value = []): array
{
return $this->recipientsService->getJiminnyRecipientsFieldData($value);
}
public function getAdditionalPromptInputFieldData(?string $value = null): array
{
return [
'id' => 'additional_prompt_input',
'label' => 'Special requirements',
'inputType' => InputTypeEnum::TEXTAREA,
'required' => false,
'placeholder' => 'What should be the focus of the report?',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCustomReportNameFieldData(?string $value = null): array
{
return [
'id' => 'custom_name',
'label' => 'Custom report name',
'inputType' => InputTypeEnum::TEXT,
'required' => false,
'placeholder' => 'Enter custom name',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
// data providers
public function getTeams(?Partner $partner = null): array
{
$teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);
$teamData = [];
foreach ($teams as $team) {
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
continue;
}
$teamData[] = $this->transformTeam($team);
}
return $teamData;
}
public function getTeamGroups(string $teamUuid): array
{
$data = [];
$team = $this->getTeam($teamUuid);
if ($team !== null) {
$groups = $team->groups()->get();
foreach ($groups as $group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
];
}
}
return $data;
}
public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array
{
$data = [];
$teams = $this->getTeams(partner: $partner);
foreach ($teams as $team) {
if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {
continue;
}
$data[] = [
'label' => $team['name'],
'groups' => $this->getTeamGroups($team['id']),
];
}
return $data;
}
public function getTeam(string $teamUuid): ?Team
{
return $this->teamRepository->idOrUuid($teamUuid);
}
public function getTeamById(int $teamId): ?Team
{
return $this->teamRepository->find($teamId);
}
public function getGroupsUuids(AutomatedReport $report): array
{
$uuids = [];
$reportGroups = $report->getGroups();
foreach ($reportGroups as $groupId) {
if ($group = $this->groupRepository->find($groupId)) {
$uuids[] = $group->getUuid();
}
}
return $uuids;
}
public function getPlaybookCategoriesUuids(AutomatedReport $report): array
{
$uuids = [];
$playbookCategories = $report->getPlaybookCategories();
foreach ($playbookCategories as $id) {
if ($category = $this->playbookCategoryRepository->find($id)) {
$uuids[] = $category->getUuid();
}
}
return $uuids;
}
public function getDealAtCallStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getDealAtCallStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getCurrentDealStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getCurrentDealStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getRecipients());
}
public function getJiminnyUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function extractUserUuids(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => $user->getUuid())
->values()
->all();
}
// get mail data
public function getRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getRecipients());
}
/**
* @return array<UserContract>
*/
public function getRecipientUserObjects(AutomatedReport $report): array
{
$userIds = $report->getRecipients()['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->values()
->all();
}
private function getJiminnyRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function buildRecipientUsers(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => [
'email' => $user->getEmailAddress(),
'name' => $user->getName(),
'timezone' => $user->getTimezone()->getName(),
])
->values()
->all();
}
public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array
{
if ($report->isAskJiminnyReport()) {
$recipients = $this->resolveAskJiminnyRecipients($report);
} else {
$recipients = $this->getRecipientUsers($report);
if ($includeJiminny) {
$recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));
}
}
$emails = [];
return array_values(array_filter(
$recipients,
static function ($recipient) use (&$emails) {
if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {
return false;
}
$emails[] = $recipient['email'];
return true;
}
));
}
private function resolveAskJiminnyRecipients(AutomatedReport $report): array
{
$recipients = [];
$creator = $report->getCreator();
if ($creator !== null) {
$recipients[] = [
'email' => $creator->getEmailAddress(),
'name' => $creator->getName(),
'timezone' => $creator->getTimezone()->getName(),
];
}
return array_merge(
$recipients,
$this->buildRecipientUsers($report->getRecipients()),
$this->getGroupRecipientUsers($report),
);
}
private function getGroupRecipientUsers(AutomatedReport $report): array
{
$users = [];
foreach ($report->getGroups() as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group === null) {
continue;
}
foreach ($group->getMembers() as $member) {
$users[] = [
'email' => $member->getEmailAddress(),
'name' => $member->getName(),
'timezone' => $member->getTimezone()->getName(),
];
}
}
return $users;
}
public function getReportTypeName(AutomatedReportResult $report): string
{
$type = $report->getReport()->getType();
$getType = $this->transformReportType($type);
return $getType['name'];
}
public function getReportPeriodName(AutomatedReportResult $report): string
{
$from = $report->getFromDate();
$to = $report->getToDate();
$frequency = $report->getReport()->getFrequency();
if ($from === null || $to === null) {
if (! $report->getReport()->isAskJiminnyReport()) {
$invalidPeriod = $from === null ? 'from' : 'to';
throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);
}
$timezone = $report->getReport()->getCreator()?->getTimezone();
$period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);
$from = $period['fromDate'];
$to = $period['toDate'];
}
return $this->formatReportPeriodName($frequency, $from, $to);
}
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 getReportTeamsName(AutomatedReportResult $report): string
{
$groups = $report->getGroups();
if (empty($groups)) {
return 'All';
}
// Get group names from repository
$groupNames = [];
foreach ($groups as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group) {
$groupNames[] = $group->getName();
}
}
if (count($groupNames) === 1) {
// Single team format
$teamsName = $groupNames[0];
} else {
// Multiple teams format
$teamsName = implode(', ', $groupNames);
}
return $teamsName;
}
public function getReportFileName(AutomatedReportResult $report): string
{
$customName = $report->getReport()->getCustomName();
$periodName = $this->getReportPeriodName($report);
$filenameSuffix = $this->getFilenameSuffix($report);
if ($customName) {
if ($filenameSuffix) {
$customName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$customName} - {$periodName}");
}
$baseName = $this->getReportTypeName($report);
if ($filenameSuffix) {
$baseName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}");
}
public function getReportFileNameWithExtension(AutomatedReportResult $result): string
{
$extension = $this->getMediaTypeMetadata($result)['extension'];
return $this->getReportFileName($result) . '.' . $extension;
}
public function sanitizeFileName(string $fileName): string
{
return str_replace(['/', '\\'], '-', $fileName);
}
public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool
{
$recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);
if (in_array($user->getId(), $recipientIds, true)) {
return true;
}
if ($report->isAskJiminnyReport()) {
$groupId = $user->getGroupId();
if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {
return true;
}
}
return false;
}
public function transformReportResults(Collection $automatedReportResults): array
{
$data = [];
foreach ($automatedReportResults as $automatedReportResult) {
/** @var AutomatedReportResult $automatedReportResult */
$report = $automatedReportResult->getReport();
$createdBy = $report->getCreator();
$creator = [
'id' => $createdBy?->getUuid(),
'name' => $createdBy?->getName(),
'email' => $createdBy?->getEmailAddress(),
'photoUrl' => $createdBy?->getPhotoUrl(),
];
$data[] = [
'id' => $automatedReportResult->getUuid(),
'name' => $automatedReportResult->getName(),
'frequency' => $this->transformFrequency($report->getFrequency()),
'recipients' => $this->buildRecipients($report),
'report_type' => $this->transformReportType($report->getType()),
'media_type' => $automatedReportResult->getMediaType(),
'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),
'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),
'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),
'creator' => $creator,
];
}
return $data;
}
private function buildRecipients(AutomatedReport $report): array
{
$creatorUuid = $report->getCreator()?->getUuid();
$recipients = array_values(array_filter(
$this->transformRecipients($report->getRecipients()),
static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,
));
if (! $report->isAskJiminnyReport()) {
return $recipients;
}
return [
...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),
...$recipients,
];
}
public function hasCallTypeConference(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);
}
public function hasCallTypeDialer(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);
}
// transformers
private function transformTeam(Team $team): array
{
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
return [];
}
return [
'id' => $team->getUuid(),
'name' => $team->getName(),
];
}
private function transformReportFullView(AutomatedReport $report): array
{
$base = $this->transformReportBase($report);
return $report->getType() === self::TYPE_ASK_JIMINNY
? $base + $this->transformAskJiminnyFields($report)
: $base + $this->transformStandardReportFields($report);
}
private function transformReportBase(AutomatedReport $report): array
{
return [
'id' => $report->getUuid(),
'organization' => $this->transformOrganization(team: $report->getTeam()),
'report_type' => $this->transformReportType($report->getType()),
'frequency' => $this->transformFrequency($report->getFrequency()),
];
}
private function transformStandardReportFields(AutomatedReport $report): array
{
$team = $report->getTeam();
return [
'report_enabled' => $report->getStatus(),
'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),
'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),
'deal_value_min' => $report->getDealValueMin(),
'deal_value_max' => $report->getDealValueMax(),
'call_types' => $this->transformCallType($report->getCallTypes()),
'media_types' => $this->transformMediaTypes($report),
'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),
'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),
'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),
'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),
'recipients' => $this->transformRecipients($report->getRecipients()),
'created_by' => $this->transformCreator($report->getCreator()),
'additional_prompt_input' => $report->getAdditionalPromptInput(),
'custom_name' => $report->getCustomName(),
'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),
'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),
'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),
];
}
private function transformAskJiminnyFields(AutomatedReport $report): array
{
$team = $report->getTeam();
$creatorId = $report->getAttribute('created_by');
$explicitUserIds = array_values(array_filter(
$report->getRecipients()['users'] ?? [],
static fn ($id) => $id !== $creatorId
));
return [
'report_name' => $report->getCustomName(),
'enabled' => $report->getStatus(),
'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),
'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),
'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),
'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),
];
}
private function transformOrganization(?Team $team): array
{
return [
'id' => $team?->getUuid(),
'name' => $team?->getName(),
];
}
private function transformReportType(string $type): array
{
foreach (self::ALL_TYPES as $typeItem) {
if ($typeItem['id'] === $type) {
return $typeItem;
}
}
return [];
}
private function transformCallType(array $types): array
{
$result = [];
$callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];
foreach ($types as $type) {
foreach ($callTypes as $callTypeItem) {
if ($callTypeItem['id'] === $type) {
$result[] = $callTypeItem;
break;
}
}
}
return $result;
}
private function transformMediaTypes(AutomatedReport $report): array
{
$values = [];
foreach ($report->getMediaTypes() as $mediaType) {
if (! in_array($mediaType, self::MEDIA_TYPES, true)) {
continue;
}
$values[] = match ($mediaType) {
self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,
self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,
};
}
return $values;
}
private function transformFrequency(string $frequency): array
{
foreach (self::ALL_FREQUENCIES as $frequencyItem) {
if ($frequencyItem['id'] === $frequency) {
return $frequencyItem;
}
}
return [];
}
public function transformDurationToMinutes(?int $duration): ?int
{
if (! $duration) {
return null;
}
return (int) ($duration / 60);
}
private function transformGroups(?Team $team, array $groupsIds): array
{
if (empty($groupsIds) || ! $team) {
return [];
}
$data = [];
foreach ($groupsIds as $groupId) {
$group = $team->groups()->where('id', $groupId)->first();
if ($group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
'photoUrl' => $group->getPhotoUrl(),
];
}
}
return $data;
}
private function transformStages(?Team $team, array $stagesIds): array
{
if (empty($stagesIds) || ! $team) {
return [];
}
$data = [];
foreach ($stagesIds as $stageId) {
$stage = $team->stages()->where('id', $stageId)->first();
if ($stage) {
$data[] = [
'id' => $stage->getUuid(),
'name' => $stage->getName(),
];
}
}
return $data;
}
private function transformRecipients(array $recipients): array
{
$users = [];
foreach ($recipients['users'] ?? [] as $userId) {
$users[] = $this->transformUser($userId);
}
return $users;
}
private function transformCreator(?User $user): ?array
{
if ($user === null) {
return null;
}
return $this->transformUser($user->getId());
}
private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array
{
if ($prompt === null) {
return null;
}
return [
'id' => $prompt->getUuid(),
'name' => $prompt->getTitle(),
];
}
private function transformSafeSearch(?Search $search): ?array
{
if ($search === null) {
return null;
}
return [
'id' => $search->getUuid(),
'name' => $search->getName(),
];
}
private function transformUser(int $userId): array
{
/* @var ?User $user */
$user = $this->userRepository->find($userId);
return [
'id' => $user?->getUuid(),
'name' => $user?->getName(),
'email' => $user?->getEmailAddress(),
'photoUrl' => $user?->getPhotoUrl(),
];
}
public function create(array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$validatedData['created_by'] = auth()->id();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
public function update(string $uuid, array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$report = $this->automatedReportsRepository->findByUuid($uuid);
if (! $report) {
throw new InvalidArgumentException('Report not found');
}
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
/**
* Create an Ask Jiminny report.
*/
public function createAskJiminnyReport(array $data, User $creator): array
{
$validatedData = $this->validateAskJiminnyReportData($data, $creator);
$validatedData['created_by'] = $creator->getId();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
return $this->transformReportFullView($automatedReport);
}
/**
* Update an Ask Jiminny report.
*/
public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array
{
if (! $report->isAskJiminnyReport()) {
throw new InvalidArgumentException('Report is not an Ask Jiminny report');
}
$validatedData = $this->validateAskJiminnyReportData($data, $user);
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
return $this->transformReportFullView($automatedReport);
}
public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array
{
if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {
throw new InvalidArgumentException(
'This report is missing a saved search or prompt. ' .
'Edit the report to complete the setup before enabling it.'
);
}
$this->automatedReportsRepository->update($report, ['status' => $status]);
return $this->transformReportFullView($report->fresh());
}
/**
* Validate and transform data for Ask Jiminny reports.
*/
private function validateAskJiminnyReportData(array $data, User $user): array
{
// Validate name
$name = trim($data['report_name'] ?? '');
if (empty($name)) {
throw new InvalidArgumentException('Report name is required');
}
if (mb_strlen($name) > 50) {
throw new InvalidArgumentException('Report name must be 50 characters or less');
}
// Validate frequency (only daily, weekly, monthly for Ask Jiminny)
$frequency = $data['frequency'] ?? null;
$askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];
if (! in_array($frequency, $askJiminnyFrequencies, true)) {
throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');
}
// Validate expiration date
$expiresAt = $data['expires_on'] ?? null;
if (empty($expiresAt)) {
throw new InvalidArgumentException('Expiration date is required');
}
try {
$expiresAtDate = Carbon::parse($expiresAt);
} catch (InvalidFormatException $e) {
throw new InvalidArgumentException('Expiration date format is invalid');
}
$maxExpiration = Carbon::now()->addYear()->endOfDay();
if ($expiresAtDate->gt($maxExpiration)) {
throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');
}
if ($expiresAtDate->isPast()) {
throw new InvalidArgumentException('Expiration date cannot be in the past');
}
// Validate saved search
$activitySearchId = $data['saved_search'] ?? null;
if (empty($activitySearchId)) {
throw new InvalidArgumentException('Saved search is required');
}
$savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);
if (! $savedSearch) {
throw new InvalidArgumentException('Saved search not found or does not belong to you');
}
// Validate saved prompt
$askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;
if (empty($askAnythingPromptId)) {
throw new InvalidArgumentException('Ask Jiminny prompt is required');
}
$prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);
if (! $prompt) {
throw new InvalidArgumentException('Ask Jiminny prompt not found');
}
// Validate status
$status = $data['enabled'] ?? false;
$recipientUserIds = [$user->getId()];
if (! empty($data['share_users'])) {
$sharedUserIds = $this->validateAndGetUserIdsByTeam(
$user->team,
(array) $data['share_users']
);
$recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);
}
$sharedGroupIds = [];
if (! empty($data['share_teams'])) {
$sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);
}
$recipientUserIds = array_values(array_unique($recipientUserIds));
return [
'team_id' => $user->getTeamId(),
'type' => self::TYPE_ASK_JIMINNY,
'status' => (bool) $status,
'frequency' => $frequency,
'custom_name' => $name,
'activity_search_id' => $savedSearch->getId(),
'ask_anything_prompt_id' => $prompt->getId(),
'expires_at' => $expiresAtDate->toDateString(),
'media_types' => [self::MEDIA_TYPE_PDF],
'call_types' => [],
'recipients' => ['users' => $recipientUserIds],
'groups' => $sharedGroupIds,
];
}
public static function getAskJiminnyFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::ASK_JIMINNY_FREQUENCIES);
}
public function getAskJiminnyReportFilters(User $user): array
{
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
return [
[
'id' => 'prompt',
'label' => 'Prompt',
'options' => $prompts,
],
[
'id' => 'saved_search',
'label' => 'Saved Search',
'options' => $savedSearches,
],
];
}
public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array
{
$team = $user->getTeam();
$userTimezone = $user->getTimezone();
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
$teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [
'id' => $group->getUuid(),
'name' => $group->getName(),
])->values()->all();
$shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];
$sharedTeamsValue = [];
$sharedUsersValue = [];
if ($report) {
$sharedTeamsValue = $this->transformGroups($team, $report->getGroups());
$recipientUserIds = $report->getRecipients()['users'] ?? [];
$creatorId = $report->getAttribute('created_by');
$sharedUserIds = array_values(array_filter(
$recipientUserIds,
static fn ($id) => $id !== $creatorId
));
$sharedUsersValue = collect($sharedUserIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (User $u) => [
'id' => $u->getUuid(),
'name' => $u->getName(),
])
->values()
->all();
}
return [
'fields' => [
[
'id' => 'enabled',
'inputType' => InputTypeEnum::TOGGLE,
'label' => '',
'value' => $report?->getStatus() ?? false,
],
[
'id' => 'report_name',
'inputType' => InputTypeEnum::TEXT,
'label' => 'Name',
'placeholder' => 'Enter name',
'required' => true,
'validation' => ['maxLength' => 50],
'value' => $report?->getCustomName() ?? '',
...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.10405585,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects<br/>Some incoming commits are not fetched<br/>","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.8194814,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskAnythingPromptServiceTest","depth":6,"bounds":{"left":0.83477396,"top":0.019952115,"width":0.080784574,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskAnythingPromptServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskAnythingPromptServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"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},"on_screen":true,"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},"on_screen":true,"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},"on_screen":true,"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},"on_screen":true,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"100","depth":4,"bounds":{"left":0.37599733,"top":0.15003991,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.3899601,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"34","depth":4,"bounds":{"left":0.39993352,"top":0.15003991,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.4192154,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"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\\Services\\Kiosk\\AutomatedReports;\n\nuse Carbon\\CarbonImmutable;\nuse Carbon\\CarbonInterface;\nuse Carbon\\Exceptions\\InvalidFormatException;\nuse DateTime;\nuse DateTimeInterface;\nuse DateTimeZone;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\InputTypeEnum;\nuse Jiminny\\Component\\AskAnything\\AskAnythingPromptService;\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Contracts\\Repositories\\PlaybookCategoryRepository;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\ModelNotFoundException;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Partner;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Repositories\\GroupRepository;\nuse Jiminny\\Repositories\\SearchRepository;\nuse Jiminny\\Repositories\\StageRepository;\nuse Throwable;\n\nclass AutomatedReportsService\n{\n public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';\n public const string TYPE_ASK_JIMINNY = 'ask_jiminny';\n\n /**\n * Standard report types (used by kiosk for existing automated reports).\n */\n // @TODO this will add filter, however if we need to control feature by FF we need conditional logic\n public const array TYPES = [\n ['id' => 'exec_summary', 'name' => 'Exec Summary'],\n ['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],\n ['id' => 'product_feedback', 'name' => 'Product Feedback'],\n ['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],\n// ['id' => 'questions', 'name' => 'Questions'],\n// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],\n ];\n\n public const array ALL_TYPES = [\n ...self::TYPES,\n ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],\n ];\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\n /**\n * Frequencies for standard (non-Ask Jiminny) reports.\n */\n public const array FREQUENCIES = [\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n\n /**\n * Frequencies for Ask Jiminny reports.\n */\n public const array ASK_JIMINNY_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ];\n\n public const string MEDIA_TYPE_PDF = 'pdf';\n public const string MEDIA_TYPE_PODCAST = 'podcast';\n public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];\n public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];\n public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];\n public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];\n\n public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];\n public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];\n public const int SENT_REPORT_AT_HOURS = 5;\n public const string PDF_KEY = 'pdf';\n public const string AUDIO_KEY = 'audio';\n\n private const array ALL_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n private const string S3_DIR = 'reports';\n private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];\n private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];\n\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly GroupRepository $groupRepository,\n private readonly UserRepository $userRepository,\n private readonly StageRepository $stageRepository,\n private readonly DealStagesService $dealStagesService,\n private readonly RecipientsService $recipientsService,\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly Webhook $webhookService,\n private readonly BusDispatcher $dispatcher,\n private readonly ActivityTypeService $activityTypeService,\n private readonly PlaybookCategoryRepository $playbookCategoryRepository,\n private readonly AskAnythingPromptService $askAnythingPromptService,\n private readonly SearchRepository $activitySearchRepository,\n private readonly AskAnythingRepository $askAnythingRepository,\n ) {\n }\n\n public static function getTypes(): array\n {\n $types = self::TYPES;\n\n return array_map(static function ($type) {\n return $type['id'];\n }, $types);\n }\n\n public static function getCallTypes(): array\n {\n return array_map(static function ($callType) {\n return $callType['id'];\n }, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);\n }\n\n public static function getFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::FREQUENCIES);\n }\n\n // front-facing structure\n public function getReportEnabledFieldData(bool $value = false): array\n {\n return [\n 'id' => 'report_enabled',\n 'label' => '',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'value' => $value,\n ];\n }\n\n // Organizations = Teams\n public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array\n {\n $options = $this->getTeams(partner: $partner);\n\n if ($shortVersion) {\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value,\n 'dependencies' => [\n 'teams',\n 'deal_stage_at_call',\n 'current_deal_stage',\n 'recipients',\n ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,\n ],\n 'dependsOn' => [],\n ];\n }\n\n // Teams = Groups\n public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array\n {\n if ($shortVersion) {\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value, // value should be an array of objects {id, name}\n 'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],\n 'dependsOn' => [],\n ];\n }\n\n public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array\n {\n $types = [];\n if ($team instanceof Team) {\n if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n $types = self::TYPES;\n }\n if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {\n $types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];\n }\n } else {\n $types = self::TYPES;\n }\n\n if ($shortVersion) {\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'options' => $types,\n ];\n }\n\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $types,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getFrequencyFieldData(?string $value = null): array\n {\n return [\n 'id' => 'frequency',\n 'label' => 'Frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::FREQUENCIES,\n 'value' => $value,\n 'dependencies' => ['period'],\n 'dependsOn' => [],\n ];\n }\n\n public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array\n {\n return [\n 'id' => 'period',\n 'label' => 'Select one-off period',\n 'inputType' => InputTypeEnum::DATE_RANGE,\n 'required' => true,\n 'placeholder' => 'Select',\n 'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],\n 'queryParams' => [\n 'startDate' => 'start_date_period',\n 'endDate' => 'end_date_period',\n ],\n 'dependencies' => [],\n 'dependsOn' => ['frequency'],\n ];\n }\n\n public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array\n {\n return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);\n }\n\n public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);\n }\n\n public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);\n }\n\n public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'deal_value',\n 'label' => 'Deal Value',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_deal_value',\n 'max' => 'max_deal_value',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array\n {\n $value = [];\n $conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;\n $dialerOn && $value[] = self::CALL_TYPE_DIALER;\n\n return [\n 'id' => 'call_type',\n 'label' => 'Call Type',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => [\n self::CALL_TYPE_CONFERENCE,\n self::CALL_TYPE_DIALER,\n ],\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getMediaTypeFieldData(?AutomatedReport $report = null): array\n {\n $value = [];\n\n if ($report) {\n $value = $this->transformMediaTypes($report);\n }\n\n return [\n 'id' => 'media_types',\n 'label' => 'Export as',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => self::MEDIA_TYPE_OBJECTS,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'call_duration',\n 'label' => 'Call Duration',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_call_duration',\n 'max' => 'max_call_duration',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getRecipientsFieldData(?Team $team = null, array $value = []): array\n {\n return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);\n }\n\n public function getJiminnyRecipientsFieldData(array $value = []): array\n {\n return $this->recipientsService->getJiminnyRecipientsFieldData($value);\n }\n\n public function getAdditionalPromptInputFieldData(?string $value = null): array\n {\n return [\n 'id' => 'additional_prompt_input',\n 'label' => 'Special requirements',\n 'inputType' => InputTypeEnum::TEXTAREA,\n 'required' => false,\n 'placeholder' => 'What should be the focus of the report?',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCustomReportNameFieldData(?string $value = null): array\n {\n return [\n 'id' => 'custom_name',\n 'label' => 'Custom report name',\n 'inputType' => InputTypeEnum::TEXT,\n 'required' => false,\n 'placeholder' => 'Enter custom name',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n // data providers\n public function getTeams(?Partner $partner = null): array\n {\n $teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);\n\n $teamData = [];\n foreach ($teams as $team) {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n continue;\n }\n\n $teamData[] = $this->transformTeam($team);\n }\n\n return $teamData;\n }\n\n public function getTeamGroups(string $teamUuid): array\n {\n $data = [];\n $team = $this->getTeam($teamUuid);\n\n if ($team !== null) {\n $groups = $team->groups()->get();\n\n foreach ($groups as $group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array\n {\n $data = [];\n $teams = $this->getTeams(partner: $partner);\n\n foreach ($teams as $team) {\n if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {\n continue;\n }\n\n $data[] = [\n 'label' => $team['name'],\n 'groups' => $this->getTeamGroups($team['id']),\n ];\n }\n\n return $data;\n }\n\n public function getTeam(string $teamUuid): ?Team\n {\n return $this->teamRepository->idOrUuid($teamUuid);\n }\n\n public function getTeamById(int $teamId): ?Team\n {\n return $this->teamRepository->find($teamId);\n }\n\n public function getGroupsUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportGroups = $report->getGroups();\n foreach ($reportGroups as $groupId) {\n if ($group = $this->groupRepository->find($groupId)) {\n $uuids[] = $group->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getPlaybookCategoriesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $playbookCategories = $report->getPlaybookCategories();\n foreach ($playbookCategories as $id) {\n if ($category = $this->playbookCategoryRepository->find($id)) {\n $uuids[] = $category->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getDealAtCallStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getDealAtCallStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getCurrentDealStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getCurrentDealStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getRecipients());\n }\n\n public function getJiminnyUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function extractUserUuids(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => $user->getUuid())\n ->values()\n ->all();\n }\n\n // get mail data\n public function getRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getRecipients());\n }\n\n /**\n * @return array<UserContract>\n */\n public function getRecipientUserObjects(AutomatedReport $report): array\n {\n $userIds = $report->getRecipients()['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->values()\n ->all();\n }\n\n private function getJiminnyRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function buildRecipientUsers(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => [\n 'email' => $user->getEmailAddress(),\n 'name' => $user->getName(),\n 'timezone' => $user->getTimezone()->getName(),\n ])\n ->values()\n ->all();\n }\n\n public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array\n {\n if ($report->isAskJiminnyReport()) {\n $recipients = $this->resolveAskJiminnyRecipients($report);\n } else {\n $recipients = $this->getRecipientUsers($report);\n if ($includeJiminny) {\n $recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));\n }\n }\n\n $emails = [];\n\n return array_values(array_filter(\n $recipients,\n static function ($recipient) use (&$emails) {\n if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {\n return false;\n }\n $emails[] = $recipient['email'];\n\n return true;\n }\n ));\n }\n\n private function resolveAskJiminnyRecipients(AutomatedReport $report): array\n {\n $recipients = [];\n\n $creator = $report->getCreator();\n if ($creator !== null) {\n $recipients[] = [\n 'email' => $creator->getEmailAddress(),\n 'name' => $creator->getName(),\n 'timezone' => $creator->getTimezone()->getName(),\n ];\n }\n\n return array_merge(\n $recipients,\n $this->buildRecipientUsers($report->getRecipients()),\n $this->getGroupRecipientUsers($report),\n );\n }\n\n private function getGroupRecipientUsers(AutomatedReport $report): array\n {\n $users = [];\n foreach ($report->getGroups() as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group === null) {\n continue;\n }\n foreach ($group->getMembers() as $member) {\n $users[] = [\n 'email' => $member->getEmailAddress(),\n 'name' => $member->getName(),\n 'timezone' => $member->getTimezone()->getName(),\n ];\n }\n }\n\n return $users;\n }\n\n public function getReportTypeName(AutomatedReportResult $report): string\n {\n $type = $report->getReport()->getType();\n\n $getType = $this->transformReportType($type);\n\n return $getType['name'];\n }\n\n public function getReportPeriodName(AutomatedReportResult $report): string\n {\n $from = $report->getFromDate();\n $to = $report->getToDate();\n $frequency = $report->getReport()->getFrequency();\n\n if ($from === null || $to === null) {\n if (! $report->getReport()->isAskJiminnyReport()) {\n $invalidPeriod = $from === null ? 'from' : 'to';\n\n throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);\n }\n\n $timezone = $report->getReport()->getCreator()?->getTimezone();\n $period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);\n $from = $period['fromDate'];\n $to = $period['toDate'];\n }\n\n return $this->formatReportPeriodName($frequency, $from, $to);\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 getReportTeamsName(AutomatedReportResult $report): string\n {\n $groups = $report->getGroups();\n\n if (empty($groups)) {\n return 'All';\n }\n\n // Get group names from repository\n $groupNames = [];\n foreach ($groups as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group) {\n $groupNames[] = $group->getName();\n }\n }\n\n if (count($groupNames) === 1) {\n // Single team format\n $teamsName = $groupNames[0];\n } else {\n // Multiple teams format\n $teamsName = implode(', ', $groupNames);\n }\n\n return $teamsName;\n }\n\n public function getReportFileName(AutomatedReportResult $report): string\n {\n $customName = $report->getReport()->getCustomName();\n $periodName = $this->getReportPeriodName($report);\n $filenameSuffix = $this->getFilenameSuffix($report);\n\n if ($customName) {\n if ($filenameSuffix) {\n $customName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$customName} - {$periodName}\");\n }\n\n $baseName = $this->getReportTypeName($report);\n\n if ($filenameSuffix) {\n $baseName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}\");\n }\n\n public function getReportFileNameWithExtension(AutomatedReportResult $result): string\n {\n $extension = $this->getMediaTypeMetadata($result)['extension'];\n\n return $this->getReportFileName($result) . '.' . $extension;\n }\n\n public function sanitizeFileName(string $fileName): string\n {\n return str_replace(['/', '\\\\'], '-', $fileName);\n }\n\n public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool\n {\n $recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);\n if (in_array($user->getId(), $recipientIds, true)) {\n return true;\n }\n\n if ($report->isAskJiminnyReport()) {\n $groupId = $user->getGroupId();\n if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {\n return true;\n }\n }\n\n return false;\n }\n\n public function transformReportResults(Collection $automatedReportResults): array\n {\n $data = [];\n foreach ($automatedReportResults as $automatedReportResult) {\n /** @var AutomatedReportResult $automatedReportResult */\n\n $report = $automatedReportResult->getReport();\n\n $createdBy = $report->getCreator();\n $creator = [\n 'id' => $createdBy?->getUuid(),\n 'name' => $createdBy?->getName(),\n 'email' => $createdBy?->getEmailAddress(),\n 'photoUrl' => $createdBy?->getPhotoUrl(),\n ];\n\n $data[] = [\n 'id' => $automatedReportResult->getUuid(),\n 'name' => $automatedReportResult->getName(),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n 'recipients' => $this->buildRecipients($report),\n 'report_type' => $this->transformReportType($report->getType()),\n 'media_type' => $automatedReportResult->getMediaType(),\n 'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),\n 'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),\n 'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),\n 'creator' => $creator,\n ];\n }\n\n return $data;\n }\n\n private function buildRecipients(AutomatedReport $report): array\n {\n $creatorUuid = $report->getCreator()?->getUuid();\n\n $recipients = array_values(array_filter(\n $this->transformRecipients($report->getRecipients()),\n static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,\n ));\n\n if (! $report->isAskJiminnyReport()) {\n return $recipients;\n }\n\n return [\n ...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),\n ...$recipients,\n ];\n }\n\n public function hasCallTypeConference(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);\n }\n\n public function hasCallTypeDialer(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);\n }\n\n // transformers\n private function transformTeam(Team $team): array\n {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n return [];\n }\n\n return [\n 'id' => $team->getUuid(),\n 'name' => $team->getName(),\n ];\n }\n\n private function transformReportFullView(AutomatedReport $report): array\n {\n $base = $this->transformReportBase($report);\n\n return $report->getType() === self::TYPE_ASK_JIMINNY\n ? $base + $this->transformAskJiminnyFields($report)\n : $base + $this->transformStandardReportFields($report);\n }\n\n private function transformReportBase(AutomatedReport $report): array\n {\n return [\n 'id' => $report->getUuid(),\n 'organization' => $this->transformOrganization(team: $report->getTeam()),\n 'report_type' => $this->transformReportType($report->getType()),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n ];\n }\n\n private function transformStandardReportFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n\n return [\n 'report_enabled' => $report->getStatus(),\n 'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),\n 'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),\n 'deal_value_min' => $report->getDealValueMin(),\n 'deal_value_max' => $report->getDealValueMax(),\n 'call_types' => $this->transformCallType($report->getCallTypes()),\n 'media_types' => $this->transformMediaTypes($report),\n 'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),\n 'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),\n 'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),\n 'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),\n 'recipients' => $this->transformRecipients($report->getRecipients()),\n 'created_by' => $this->transformCreator($report->getCreator()),\n 'additional_prompt_input' => $report->getAdditionalPromptInput(),\n 'custom_name' => $report->getCustomName(),\n 'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),\n 'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),\n 'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),\n ];\n }\n\n private function transformAskJiminnyFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n $creatorId = $report->getAttribute('created_by');\n $explicitUserIds = array_values(array_filter(\n $report->getRecipients()['users'] ?? [],\n static fn ($id) => $id !== $creatorId\n ));\n\n return [\n 'report_name' => $report->getCustomName(),\n 'enabled' => $report->getStatus(),\n 'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),\n 'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),\n 'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),\n 'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),\n ];\n }\n\n private function transformOrganization(?Team $team): array\n {\n return [\n 'id' => $team?->getUuid(),\n 'name' => $team?->getName(),\n ];\n }\n\n private function transformReportType(string $type): array\n {\n foreach (self::ALL_TYPES as $typeItem) {\n if ($typeItem['id'] === $type) {\n return $typeItem;\n }\n }\n\n return [];\n }\n\n private function transformCallType(array $types): array\n {\n $result = [];\n $callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];\n\n foreach ($types as $type) {\n foreach ($callTypes as $callTypeItem) {\n if ($callTypeItem['id'] === $type) {\n $result[] = $callTypeItem;\n\n break;\n }\n }\n }\n\n return $result;\n }\n\n private function transformMediaTypes(AutomatedReport $report): array\n {\n $values = [];\n\n foreach ($report->getMediaTypes() as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n continue;\n }\n\n $values[] = match ($mediaType) {\n self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,\n self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,\n };\n }\n\n return $values;\n }\n\n private function transformFrequency(string $frequency): array\n {\n foreach (self::ALL_FREQUENCIES as $frequencyItem) {\n if ($frequencyItem['id'] === $frequency) {\n return $frequencyItem;\n }\n }\n\n return [];\n }\n\n public function transformDurationToMinutes(?int $duration): ?int\n {\n if (! $duration) {\n return null;\n }\n\n return (int) ($duration / 60);\n }\n\n private function transformGroups(?Team $team, array $groupsIds): array\n {\n if (empty($groupsIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($groupsIds as $groupId) {\n $group = $team->groups()->where('id', $groupId)->first();\n\n if ($group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n 'photoUrl' => $group->getPhotoUrl(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformStages(?Team $team, array $stagesIds): array\n {\n if (empty($stagesIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($stagesIds as $stageId) {\n $stage = $team->stages()->where('id', $stageId)->first();\n\n if ($stage) {\n $data[] = [\n 'id' => $stage->getUuid(),\n 'name' => $stage->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformRecipients(array $recipients): array\n {\n $users = [];\n foreach ($recipients['users'] ?? [] as $userId) {\n $users[] = $this->transformUser($userId);\n }\n\n return $users;\n }\n\n private function transformCreator(?User $user): ?array\n {\n if ($user === null) {\n return null;\n }\n\n return $this->transformUser($user->getId());\n }\n\n private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array\n {\n if ($prompt === null) {\n return null;\n }\n\n return [\n 'id' => $prompt->getUuid(),\n 'name' => $prompt->getTitle(),\n ];\n }\n\n private function transformSafeSearch(?Search $search): ?array\n {\n if ($search === null) {\n return null;\n }\n\n return [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ];\n }\n\n private function transformUser(int $userId): array\n {\n /* @var ?User $user */\n $user = $this->userRepository->find($userId);\n\n return [\n 'id' => $user?->getUuid(),\n 'name' => $user?->getName(),\n 'email' => $user?->getEmailAddress(),\n 'photoUrl' => $user?->getPhotoUrl(),\n ];\n }\n\n public function create(array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $validatedData['created_by'] = auth()->id();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function update(string $uuid, array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $report = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $report) {\n throw new InvalidArgumentException('Report not found');\n }\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Create an Ask Jiminny report.\n */\n public function createAskJiminnyReport(array $data, User $creator): array\n {\n $validatedData = $this->validateAskJiminnyReportData($data, $creator);\n $validatedData['created_by'] = $creator->getId();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Update an Ask Jiminny report.\n */\n public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array\n {\n if (! $report->isAskJiminnyReport()) {\n throw new InvalidArgumentException('Report is not an Ask Jiminny report');\n }\n\n $validatedData = $this->validateAskJiminnyReportData($data, $user);\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array\n {\n if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {\n throw new InvalidArgumentException(\n 'This report is missing a saved search or prompt. ' .\n 'Edit the report to complete the setup before enabling it.'\n );\n }\n\n $this->automatedReportsRepository->update($report, ['status' => $status]);\n\n return $this->transformReportFullView($report->fresh());\n }\n\n /**\n * Validate and transform data for Ask Jiminny reports.\n */\n private function validateAskJiminnyReportData(array $data, User $user): array\n {\n // Validate name\n $name = trim($data['report_name'] ?? '');\n if (empty($name)) {\n throw new InvalidArgumentException('Report name is required');\n }\n if (mb_strlen($name) > 50) {\n throw new InvalidArgumentException('Report name must be 50 characters or less');\n }\n\n // Validate frequency (only daily, weekly, monthly for Ask Jiminny)\n $frequency = $data['frequency'] ?? null;\n $askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];\n if (! in_array($frequency, $askJiminnyFrequencies, true)) {\n throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');\n }\n\n // Validate expiration date\n $expiresAt = $data['expires_on'] ?? null;\n if (empty($expiresAt)) {\n throw new InvalidArgumentException('Expiration date is required');\n }\n\n try {\n $expiresAtDate = Carbon::parse($expiresAt);\n } catch (InvalidFormatException $e) {\n throw new InvalidArgumentException('Expiration date format is invalid');\n }\n $maxExpiration = Carbon::now()->addYear()->endOfDay();\n if ($expiresAtDate->gt($maxExpiration)) {\n throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');\n }\n if ($expiresAtDate->isPast()) {\n throw new InvalidArgumentException('Expiration date cannot be in the past');\n }\n\n // Validate saved search\n $activitySearchId = $data['saved_search'] ?? null;\n if (empty($activitySearchId)) {\n throw new InvalidArgumentException('Saved search is required');\n }\n $savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);\n if (! $savedSearch) {\n throw new InvalidArgumentException('Saved search not found or does not belong to you');\n }\n\n // Validate saved prompt\n $askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;\n if (empty($askAnythingPromptId)) {\n throw new InvalidArgumentException('Ask Jiminny prompt is required');\n }\n $prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);\n if (! $prompt) {\n throw new InvalidArgumentException('Ask Jiminny prompt not found');\n }\n\n // Validate status\n $status = $data['enabled'] ?? false;\n\n $recipientUserIds = [$user->getId()];\n\n if (! empty($data['share_users'])) {\n $sharedUserIds = $this->validateAndGetUserIdsByTeam(\n $user->team,\n (array) $data['share_users']\n );\n $recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);\n }\n\n $sharedGroupIds = [];\n if (! empty($data['share_teams'])) {\n $sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);\n }\n\n $recipientUserIds = array_values(array_unique($recipientUserIds));\n\n return [\n 'team_id' => $user->getTeamId(),\n 'type' => self::TYPE_ASK_JIMINNY,\n 'status' => (bool) $status,\n 'frequency' => $frequency,\n 'custom_name' => $name,\n 'activity_search_id' => $savedSearch->getId(),\n 'ask_anything_prompt_id' => $prompt->getId(),\n 'expires_at' => $expiresAtDate->toDateString(),\n 'media_types' => [self::MEDIA_TYPE_PDF],\n 'call_types' => [],\n 'recipients' => ['users' => $recipientUserIds],\n 'groups' => $sharedGroupIds,\n ];\n }\n\n public static function getAskJiminnyFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::ASK_JIMINNY_FREQUENCIES);\n }\n\n public function getAskJiminnyReportFilters(User $user): array\n {\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n return [\n [\n 'id' => 'prompt',\n 'label' => 'Prompt',\n 'options' => $prompts,\n ],\n [\n 'id' => 'saved_search',\n 'label' => 'Saved Search',\n 'options' => $savedSearches,\n ],\n ];\n }\n\n public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array\n {\n $team = $user->getTeam();\n $userTimezone = $user->getTimezone();\n\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n $teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ])->values()->all();\n\n $shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];\n\n $sharedTeamsValue = [];\n $sharedUsersValue = [];\n if ($report) {\n $sharedTeamsValue = $this->transformGroups($team, $report->getGroups());\n\n $recipientUserIds = $report->getRecipients()['users'] ?? [];\n $creatorId = $report->getAttribute('created_by');\n $sharedUserIds = array_values(array_filter(\n $recipientUserIds,\n static fn ($id) => $id !== $creatorId\n ));\n $sharedUsersValue = collect($sharedUserIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (User $u) => [\n 'id' => $u->getUuid(),\n 'name' => $u->getName(),\n ])\n ->values()\n ->all();\n }\n\n return [\n 'fields' => [\n [\n 'id' => 'enabled',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'label' => '',\n 'value' => $report?->getStatus() ?? false,\n ],\n [\n 'id' => 'report_name',\n 'inputType' => InputTypeEnum::TEXT,\n 'label' => 'Name',\n 'placeholder' => 'Enter name',\n 'required' => true,\n 'validation' => ['maxLength' => 50],\n 'value' => $report?->getCustomName() ?? '',\n ],\n [\n 'id' => 'frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Frequency',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::ASK_JIMINNY_FREQUENCIES,\n 'value' => $report ? $this->transformFrequency($report->getFrequency()) : null,\n ],\n [\n 'id' => 'expires_on',\n 'inputType' => InputTypeEnum::DATE,\n 'label' => 'Expires on',\n 'required' => true,\n 'placeholder' => 'Select',\n 'validation' => [\n 'minDate' => now($userTimezone)->toDateString(),\n 'maxDate' => now($userTimezone)->addYear()->toDateString(),\n ],\n 'value' => $report?->getExpiresAt()?->toDateString(),\n ],\n [\n 'id' => 'share_teams',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team',\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $teamGroups,\n 'value' => $sharedTeamsValue,\n ],\n [\n 'id' => 'share_users',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team member',\n 'required' => false,\n 'placeholder' => 'Select',\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'options' => $shareUsers,\n 'value' => $sharedUsersValue,\n ],\n [\n 'id' => 'saved_search',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Saved search',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $savedSearches,\n 'value' => $report && $report->getSavedSearch() ? [\n 'id' => $report->getSavedSearch()->getUuid(),\n 'name' => $report->getSavedSearch()->getName(),\n ] : null,\n ],\n [\n 'id' => 'ask_jiminny_prompt',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Ask Jiminny prompt',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $prompts,\n 'value' => $report && $report->getAskAnythingPrompt() ? [\n 'id' => $report->getAskAnythingPrompt()->getUuid(),\n 'name' => $report->getAskAnythingPrompt()->getTitle(),\n ] : null,\n ],\n ],\n ];\n }\n\n private function updateResultNames(AutomatedReport $automatedReport): void\n {\n $results = $this->automatedReportsRepository->getResultsByReport($automatedReport);\n\n foreach ($results as $result) {\n $result->update(['name' => $this->getReportFileName($result)]);\n }\n }\n\n public function updateStatus(string $uuid, array $data): array\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $automatedReport->update([\n 'status' => $status,\n ]);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n private function generateOneOffReport(AutomatedReport $automatedReport): void\n {\n // the scheduler handles all the other frequency types\n if ($automatedReport->getStatus() === false || $automatedReport->getFrequency() !== self::FREQUENCY_ONE_OFF) {\n return;\n }\n\n $this->dispatcher->dispatch(new RequestGenerateReportJob($automatedReport->getUuid()));\n }\n\n public function getReport(string $uuid, ?Partner $partner = null): AutomatedReport\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n if ($partner !== null && ! $partner->isDefaultPartner() && $automatedReport->team->partner_id !== $partner->getId()) {\n throw new ModelNotFoundException('Report not found');\n }\n\n return $automatedReport;\n }\n\n public function get(string $uuid, ?Partner $partner = null): array\n {\n $automatedReport = $this->getReport($uuid, $partner);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function list(string $sortColumn = 'created_at', string $sortDirection = 'desc', ?Partner $partner = null): array\n {\n $results = [];\n $collection = $this->automatedReportsRepository->getAllStandardReports($sortColumn, $sortDirection, $partner);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function listAskJiminnyReports(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): array {\n $results = [];\n $collection = $this->automatedReportsRepository->getAskJiminnyReportsByUser($user, $sortColumn, $sortDirection);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function delete(string $uuid): void\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $automatedReport->delete();\n }\n\n public function createReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n return $this->automatedReportsRepository->createResult(\n array_merge(\n [\n 'report_id' => $automatedReport->getId(),\n 'status' => AutomatedReportResult::STATUS_DEFAULT,\n ],\n $data\n )\n );\n }\n\n public function getOrCreateReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n $existing = $this->automatedReportsRepository->findLatestSameDayDefaultOrFailedResult($automatedReport);\n\n if ($existing !== null) {\n $existing->update(['status' => AutomatedReportResult::STATUS_DEFAULT]);\n\n return $existing;\n }\n\n return $this->createReportResult($automatedReport, $data);\n }\n\n public function getReportResult(string $resultUuid): AutomatedReportResult\n {\n $report = $this->automatedReportsRepository->findResultByUuid($resultUuid);\n\n if (! $report) {\n throw new ModelNotFoundException('Report Result not found');\n }\n\n return $report;\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return $this->automatedReportsRepository->findChildResult($result, $type);\n }\n\n // prophet API calls\n /**\n * @throws ApplicationException\n */\n public function getGenerateReportPayload(AutomatedReport $automatedReport, string $reportResultUuid): array\n {\n $period = $this->calculateFromAndToDate($automatedReport);\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n return [\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResultUuid,\n 'report_type' => $automatedReport->getType(),\n 'media_types' => $automatedReport->getMediaTypes(),\n 'from_date' => $fromDate->startOfDay()->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->endOfDay()->format(DateTimeInterface::RFC3339),\n 'group_ids' => $automatedReport->getGroups(),\n 'call_deal_stage' => $automatedReport->getDealAtCallStages(),\n 'current_deal_stage' => $automatedReport->getCurrentDealStages(),\n 'deal_min_value' => $automatedReport->getDealValueMin(),\n 'deal_max_value' => $automatedReport->getDealValueMax(),\n 'call_types' => $automatedReport->getCallTypes(),\n 'call_duration_min_seconds' => $automatedReport->getCallDurationMin(),\n 'call_duration_max_seconds' => $automatedReport->getCallDurationMax(),\n 'special_requirements' => $automatedReport->getAdditionalPromptInput(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->formatReportPeriodName(\n $automatedReport->getFrequency(),\n $fromDate,\n $toDate,\n ),\n 'playbook_categories' => $automatedReport->getPlaybookCategories(),\n 'custom_name' => $automatedReport->getCustomName(),\n ];\n }\n\n // $inputPayload - FE payload structure\n public function getActivitiesCountPayload(array $inputPayload): array\n {\n // Use validateAndTransformData to validate and normalize input\n $validatedData = $this->validateAndTransformData($inputPayload);\n $period = $this->calculateFromAndToDatePeriod(\n $validatedData['frequency'],\n Carbon::parse($validatedData['from']),\n Carbon::parse($validatedData['to']),\n );\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n // Create payload similar to getGenerateReportPayload\n return [\n 'team_id' => $validatedData['team_id'],\n 'group_ids' => $validatedData['groups'] ?? [],\n 'report_type' => $validatedData['type'],\n 'from_date' => $fromDate->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->format(DateTimeInterface::RFC3339),\n 'call_deal_stage' => $validatedData['deal_at_call_stages'] ?? [],\n 'current_deal_stage' => $validatedData['current_deal_stages'] ?? [],\n 'deal_min_value' => $validatedData['deal_value_min'] ?? null,\n 'deal_max_value' => $validatedData['deal_value_max'] ?? null,\n 'call_types' => $validatedData['call_types'],\n 'call_duration_min_seconds' => $validatedData['call_duration_min'] ?? null,\n 'call_duration_max_seconds' => $validatedData['call_duration_max'] ?? null,\n 'special_requirements' => $validatedData['additional_prompt_input'] ?? null,\n 'playbook_categories' => $validatedData['playbook_categories'] ?? [],\n 'request_id' => null,\n 'callback_url' => null,\n ];\n }\n\n public function shouldSendReport(array $users, ?CarbonInterface $generatedAt = null): bool\n {\n if (empty($users)) {\n return false;\n }\n\n $earliestTz = collect($users)\n ->mapWithKeys(function (array $user) {\n $tz = new DateTimeZone($user['timezone']);\n $nowUtc = new DateTime('now', new DateTimeZone('UTC'));\n $offset = $tz->getOffset($nowUtc);\n\n return [$user['timezone'] => $offset];\n })\n ->sortDesc()\n ->keys()\n ->first();\n\n $now = Carbon::now($earliestTz);\n $isScheduledTime = (int) $now->format('H') === self::SENT_REPORT_AT_HOURS;\n\n if ($isScheduledTime) {\n return true;\n }\n\n return $this->hasPassedScheduledTime($generatedAt, $earliestTz);\n }\n\n public function hasPassedScheduledTime(?CarbonInterface $generatedAt, string $timezone): bool\n {\n if ($generatedAt === null) {\n return false;\n }\n\n $now = Carbon::now($timezone);\n $scheduledTime = $now->copy()->setTime(self::SENT_REPORT_AT_HOURS, 0, 0);\n\n if ($now->hour < self::SENT_REPORT_AT_HOURS) {\n $scheduledTime = $scheduledTime->subDay();\n }\n\n $scheduledTimeUtc = $scheduledTime->copy()->utc();\n $generatedAtUtc = $generatedAt->copy()->utc();\n $nowUtc = $now->copy()->utc();\n\n return $generatedAtUtc->lt($scheduledTimeUtc) && $nowUtc->gt($scheduledTimeUtc);\n }\n\n public function calculateFromAndToDatePeriod(\n string $frequency,\n ?Carbon $fromDate = null,\n ?Carbon $toDate = null,\n DateTimeZone|string|null $timezone = 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($timezone);\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()->subWeek()->startOfWeek(CarbonInterface::MONDAY),\n 'toDate' => $now->copy()->subWeek()->endOfWeek(CarbonInterface::SUNDAY),\n ],\n self::FREQUENCY_MONTHLY => [\n 'fromDate' => $now->copy()->subMonthNoOverflow()->startOfMonth(),\n 'toDate' => $now->copy()->subMonthNoOverflow()->endOfMonth(),\n ],\n self::FREQUENCY_QUARTERLY => [\n 'fromDate' => $now->copy()->subQuarterNoOverflow()->startOfQuarter(),\n 'toDate' => $now->copy()->subQuarterNoOverflow()->endOfQuarter(),\n ],\n default => throw new InvalidArgumentException(\"Unsupported frequency: {$frequency}\"),\n };\n }\n\n private function calculateFromAndToDate(AutomatedReport $automatedReport): array\n {\n return $this->calculateFromAndToDatePeriod(\n $automatedReport->getFrequency(),\n $automatedReport->getFrom(),\n $automatedReport->getTo()\n );\n }\n\n public function getAskJiminnyGenerateReportPayload(\n AutomatedReport $automatedReport,\n AutomatedReportResult $reportResult,\n array $activityIds,\n ): array {\n return [\n 'user_question' => $automatedReport->getAskAnythingPrompt()?->getContent(),\n 'call_ids' => array_map('strval', $activityIds),\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResult->getUuid(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->getReportPeriodName($reportResult),\n 'report_name' => $automatedReport->getCustomName(),\n ];\n }\n\n private function getCallbackUrl(): string\n {\n return $this->webhookService->route('jiminny.webhook.reports.ready');\n }\n\n /**\n * Validate and transform payload data for automated reports\n *\n * @param array $data\n *\n * @throws InvalidArgumentException\n *\n * @return array\n */\n private function validateAndTransformData(array $data): array\n {\n // Validate organization (team) and check feature\n $team = $this->validateOrganization($data['organization'] ?? null);\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $type = $this->validateReportType($data['report_type'] ?? null);\n $frequency = $this->validateFrequency($data['frequency'] ?? null);\n $additionalPromptInput = $this->validateAdditionalPromptInput(\n $data['additional_prompt_input'] ?? null\n );\n $customReportName = $this->validateCustomReportName($data['custom_name'] ?? null);\n\n // Prepare data for the database\n $reportData = [\n 'team_id' => $team->getId(),\n 'type' => $type,\n 'status' => $status,\n 'frequency' => $frequency,\n 'additional_prompt_input' => $additionalPromptInput,\n 'custom_name' => $customReportName,\n ];\n\n // Validate deal values\n $reportData = $this->validateDealValues($data, $reportData);\n\n // Validate date range\n $reportData = $this->validateDateRange($data, $reportData, $frequency);\n\n // Validate call durations\n $reportData = $this->validateCallDurations($data, $reportData);\n\n // Validate call types\n $reportData = $this->validateCallTypes($data, $reportData);\n\n // Validate media types\n $reportData = $this->validateMediaTypes($data, $reportData);\n\n // Validate groups\n if (isset($data['teams'])) {\n $reportData['groups'] = $this->validateAndGetGroupIds($team, $data['teams']);\n }\n\n // Validate deal stages\n $reportData = $this->validateDealStages($data, $reportData, $team, $type);\n\n // Validate playbook categories\n $reportData = $this->validatePlaybookCategories($data, $reportData, $team);\n\n // Validate recipients\n $reportData['recipients'] = [\n 'users' => $this->validateAndGetUserIdsByTeam($team, $data['recipients'] ?? []),\n ];\n\n if (isset($data['jiminny_recipients'])) {\n // Validate Jiminny recipients\n $reportData['jiminny_recipients'] = [\n 'users' => $this->validateAndGetJiminnyUserIds((array) $data['jiminny_recipients']),\n ];\n }\n\n return $reportData;\n }\n\n private function validateDealValues(array $data, array $reportData): array\n {\n if (isset($data['min_deal_value'])) {\n $reportData['deal_value_min'] = (int) $data['min_deal_value'];\n\n if ($reportData['deal_value_min'] > 4294967295 || $reportData['deal_value_min'] < 0) {\n throw new InvalidArgumentException('Min deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_deal_value'])) {\n $reportData['deal_value_max'] = (int) $data['max_deal_value'];\n\n if ($reportData['deal_value_max'] > 4294967295 || $reportData['deal_value_max'] < 0) {\n throw new InvalidArgumentException('Max deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['min_deal_value'], $data['max_deal_value'])\n && $data['min_deal_value'] > $data['max_deal_value']\n ) {\n throw new InvalidArgumentException('Min deal value cannot be greater than max deal value');\n }\n\n return $reportData;\n }\n\n private function validateDateRange(array $data, array $reportData, string $frequency): array\n {\n // Set date range only for one_off frequency\n if ($frequency === 'one_off') {\n if (isset($data['start_date_period'])) {\n $reportData['from'] = $this->parseDate($data['start_date_period']);\n }\n\n if (isset($data['end_date_period'])) {\n $reportData['to'] = $this->parseDate($data['end_date_period']);\n }\n\n if (empty($reportData['from']) || empty($reportData['to'])) {\n throw new InvalidArgumentException(\n 'Start date and end date are required for one_off frequency'\n );\n }\n } else {\n $reportData['from'] = null;\n $reportData['to'] = null;\n }\n\n return $reportData;\n }\n\n private function validateCallDurations(array $data, array $reportData): array\n {\n // Convert call durations from minutes to seconds\n if (isset($data['min_call_duration'])) {\n $reportData['call_duration_min'] = (int) $data['min_call_duration'] * 60;\n\n if ($reportData['call_duration_min'] > 4294967295 || $reportData['call_duration_min'] < 0) {\n throw new InvalidArgumentException('Min call duration should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_call_duration'])) {\n $reportData['call_duration_max'] = (int) $data['max_call_duration'] * 60;\n\n if ($reportData['call_duration_max'] > 4294967295 || $reportData['call_duration_max'] < 0) {\n throw new InvalidArgumentException('Max call duration should be between 0 and 4294967295');\n }\n }\n\n return $reportData;\n }\n\n private function validateCallTypes(array $data, array $reportData): array\n {\n // Set call types\n $reportData['call_types'] = $data['call_type'] ?? [];\n if (empty($reportData['call_types'])) {\n $reportData['call_types'] = self::getCallTypes();\n }\n\n foreach ($reportData['call_types'] as $callType) {\n if (! in_array($callType, self::getCallTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Call type %s is invalid', $callType));\n }\n }\n\n return $reportData;\n }\n\n private function validateMediaTypes(array $data, array $reportData): array\n {\n // Set media types from input data\n $reportData['media_types'] = $data['media_types'] ?? [];\n\n if (empty($reportData['media_types'])) {\n throw new InvalidArgumentException('Media types are required');\n }\n\n foreach ($reportData['media_types'] as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n throw new InvalidArgumentException(sprintf('Media type %s is invalid', $mediaType));\n }\n }\n\n return $reportData;\n }\n\n private function validateDealStages(array $data, array $reportData, Team $team, string $reportType): array\n {\n // Validate and set deal stages\n if (isset($data['deal_stage_at_call'])) {\n $reportData['deal_at_call_stages'] =\n $this->validateAndGetDealStageIds($team, $data['deal_stage_at_call'], 'Deal stage at call');\n }\n\n if (isset($data['current_deal_stage'])) {\n $reportData['current_deal_stages'] =\n $this->validateAndGetDealStageIds($team, $data['current_deal_stage'], 'Current deal stage');\n }\n\n // Ensure current_deal_stage is not provided for loss_analysis report type\n if ($reportType === self::TYPE_LOSS_ANALYSIS && ! empty($data['current_deal_stage'])) {\n throw new InvalidArgumentException('Current deal stage is not applicable for Loss Analysis reports');\n }\n\n return $reportData;\n }\n\n // transform uuid to id\n private function validatePlaybookCategories(array $data, array $reportData, Team $team): array\n {\n $key = 'playbook_categories';\n\n if (isset($data[$key])) {\n $payloadIds = $data[$key];\n $ids = [];\n\n foreach ($payloadIds as $uuid) {\n $uuid = (string) $uuid;\n\n try {\n $playbookCategory = $this->playbookCategoryRepository->findByUuid($uuid);\n } catch (Throwable $throwable) {\n Log::error(__METHOD__ . ' ' . $throwable->getMessage());\n\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory) {\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory->hasPlaybook()) {\n throw new InvalidArgumentException(sprintf('Playbook category %s has no playbook', $uuid));\n }\n\n if ($playbookCategory->getPlaybook()->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Playbook category %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $ids[] = $playbookCategory->getId();\n }\n\n $reportData[$key] = $ids;\n }\n\n return $reportData;\n }\n\n private function validateReportStatus($status): bool\n {\n if (! in_array($status, [true, false], true)) {\n throw new InvalidArgumentException('Report status is invalid');\n }\n\n return $status;\n }\n\n private function validateReportType($type): string\n {\n if (! in_array($type, self::getTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Report type is invalid: %s', $type));\n }\n\n return $type;\n }\n\n private function validateFrequency($frequency): string\n {\n if (! in_array($frequency, self::getFrequencies(), true)) {\n throw new InvalidArgumentException('Frequency is invalid');\n }\n\n return $frequency;\n }\n\n private function validateAdditionalPromptInput(?string $additionalPromptInput): ?string\n {\n if ($additionalPromptInput && strlen($additionalPromptInput) > 5000) {\n throw new InvalidArgumentException('Additional Prompt Input should be less than 5000 characters');\n }\n\n return $additionalPromptInput;\n }\n\n private function validateCustomReportName(?string $customReportName): ?string\n {\n if ($customReportName === null || $customReportName === '') {\n return null;\n }\n\n if (strlen($customReportName) > 70) {\n throw new InvalidArgumentException('Custom report name should be less than 70 characters');\n }\n\n return $customReportName;\n }\n\n private function validateOrganization(?string $organizationUuid): Team\n {\n if (! $organizationUuid) {\n throw new InvalidArgumentException('Organization is required');\n }\n\n $team = $this->teamRepository->idOrUuid($organizationUuid);\n\n if (! $team) {\n throw new InvalidArgumentException('Organization not found');\n }\n\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n throw new InvalidArgumentException('Organization does not have the Automated Reports feature');\n }\n\n return $team;\n }\n\n private function validateAndGetGroupIds(Team $team, array $teamUuids): array\n {\n $groupIds = [];\n\n foreach ($teamUuids as $uuid) {\n $group = $this->groupRepository->findByUuid($uuid);\n\n if ($group === null || $group->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Group %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $groupIds[] = $group->getId();\n\n }\n\n return $groupIds;\n }\n\n private function validateAndGetDealStageIds(Team $team, array $stageUuids, string $propertyLabel): array\n {\n $stageIds = [];\n\n foreach ($stageUuids as $uuid) {\n $stage = $this->stageRepository->findByUuid($uuid);\n\n if ($stage === null || $stage->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Stage %s not found for team %s for %s', $uuid, $team->getUuid(), $propertyLabel)\n );\n }\n\n $stageIds[] = $stage->getId();\n }\n\n return $stageIds;\n }\n\n private function validateAndGetUserIds(array $userUuids, callable $teamCheck): array\n {\n if (empty($userUuids)) {\n return [];\n }\n\n $userIds = [];\n\n foreach ($userUuids as $uuid) {\n $user = $this->userRepository->findByUuid($uuid);\n\n if (! $user || ! $user->isStatusActive()) {\n throw new InvalidArgumentException(\n sprintf('User %s not found or is not active', $uuid)\n );\n }\n\n if (! $teamCheck($user)) {\n throw new InvalidArgumentException(\n sprintf('User %s does not belong to the allowed team(s)', $uuid)\n );\n }\n\n $userIds[] = $user->getId();\n }\n\n return $userIds;\n }\n\n private function validateAndGetUserIdsByTeam(Team $team, array $userUuids): array\n {\n return $this->validateAndGetUserIds($userUuids, fn ($user) => $user->getTeamId() === $team->getId());\n }\n\n private function validateAndGetJiminnyUserIds(array $userUuids): array\n {\n $allowedTeamIds = config('kiosk.teamIds', []);\n\n return $this->validateAndGetUserIds($userUuids, fn ($user) => in_array($user->getTeamId(), $allowedTeamIds, true));\n }\n\n private function parseDate(string $dateString): string\n {\n return date('Y-m-d H:i:s', strtotime($dateString));\n }\n\n private function generateReportResultViewUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.view', ['uuid' => $result->getUuid()]);\n }\n\n private function generateReportResultDownloadUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.download', ['uuid' => $result->getUuid()]);\n }\n\n private function getReportMediaRouteResource(AutomatedReportResult $result): string\n {\n if ($result->getMediaType() === self::MEDIA_TYPE_PDF) {\n return self::PDF_KEY;\n } elseif ($result->getMediaType() === self::MEDIA_TYPE_PODCAST) {\n return self::AUDIO_KEY;\n }\n\n throw new \\InvalidArgumentException('Unknown media type.');\n }\n\n public function getMediaPath(AutomatedReportResult $result): ?string\n {\n $url = match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => $result->getPdfUrl(),\n self::MEDIA_TYPE_PODCAST => $result->getPodcastAudioUrl(),\n default => null,\n };\n\n if ($url === null) {\n return null;\n }\n\n $path = parse_url(trim($url, '\"\\''), PHP_URL_PATH);\n\n return $path ?: null;\n }\n\n public function getFilenameSuffix(AutomatedReportResult $result): ?string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => 'Podcast',\n default => null,\n };\n }\n\n public function getMailSubjectSuffix(AutomatedReportResult $result): string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => 'report',\n self::MEDIA_TYPE_PODCAST => 'podcast',\n default => '',\n };\n }\n\n public function getMediaTypeMetadata(AutomatedReportResult $result): array\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => ['extension' => 'mp3', 'mime' => 'audio/mpeg'],\n self::MEDIA_TYPE_PDF => ['extension' => 'pdf', 'mime' => 'application/pdf'],\n default => ['extension' => null, 'mime' => null],\n };\n }\n\n public function deleteS3Files(AutomatedReportResult $result): void\n {\n $teamUuid = $result->getReport()->getTeam()->getUuid();\n $reportUuid = $result->getUuid();\n\n // delete all files for a report uuid no mather of pdf, podcast, or both\n // in case of both - the podcast files are linked to the pdf (parent) uuid\n // pdf and podcast date times should be close\n $path = sprintf('%s/%s/%s', $teamUuid, self::S3_DIR, $reportUuid);\n\n foreach (self::FILE_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted S3 file', [\n 'path' => $file,\n ]);\n }\n }\n\n foreach (self::FILE_PODCAST_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '_podcast.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted Podcast S3 file', [\n 'path' => $file,\n ]);\n }\n }\n }\n\n /**\n *\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): Collection\n {\n return $this->automatedReportsRepository->getTeamIdsWithReportsResults($teamId);\n }\n\n /**\n * Core delete logic for report results using a query\n *\n * @param Builder $query\n * @param array $logContext\n *\n * @return int\n */\n private function deleteReportResultsByQuery(Builder $query, array $logContext = []): int\n {\n $deletedCount = 0;\n\n if ($query->exists()) {\n Log::info(\n 'Run delete report results',\n array_merge(\n $logContext,\n [\n 'service' => 'AutomatedReportsService',\n ]\n )\n );\n\n $query->chunkById(50, function ($results) use (&$deletedCount, $logContext) {\n foreach ($results as $result) {\n $this->deleteReportResult($result);\n $deletedCount++;\n\n Log::info(\n 'Deleted a report result',\n array_merge(\n $logContext,\n [\n 'result_id' => $result->getId(),\n 'report_id' => $result->getReportId(),\n ]\n )\n );\n }\n });\n }\n\n return $deletedCount;\n }\n\n /**\n * Delete report results for a team by retention period\n *\n * @param Team $team\n * @param CarbonImmutable $retentionDate\n *\n * @return int Number of deleted report results\n */\n public function deleteReportsResultsInRetentionPeriod(Team $team, CarbonImmutable $retentionDate): int\n {\n $reportIds = $this->automatedReportsRepository->getReportIdsByTeam($team);\n\n if ($reportIds->isEmpty()) {\n return 0;\n }\n\n $query = $this->automatedReportsRepository\n ->getReportResultsQueryForRetention($team, $retentionDate);\n\n return $this->deleteReportResultsByQuery($query, [\n 'team_id' => $team->getId(),\n 'retention_date' => $retentionDate->toDateTimeString(),\n ]);\n }\n\n /**\n * Delete ALL report results for a specific automated report\n *\n * @param string $uuid\n *\n * @return int\n */\n public function deleteReportResults(string $uuid): int\n {\n $report = $this->getReport($uuid);\n\n $query = $this->automatedReportsRepository->getResultsByReportQuery($report);\n\n return $this->deleteReportResultsByQuery($query, [\n 'report_uuid' => $uuid,\n 'report_id' => $report->getId(),\n ]);\n }\n\n public function deleteReportResult(AutomatedReportResult $result): void\n {\n $this->deleteS3Files($result);\n\n $result->delete();\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getTeamReports(Team $team): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getReportsByTeam($team);\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getReportResults(AutomatedReport $report): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getResultsByReport($report);\n }\n\n public function deleteAllReportResults(AutomatedReport $report): void\n {\n $results = $this->getReportResults($report);\n\n /** @var AutomatedReportResult $result */\n foreach ($results as $result) {\n Log::info('Deleting result', [\n 'report' => $report->getId(),\n 'result' => $result->getId(),\n ]);\n\n $this->deleteReportResult($result);\n }\n }\n\n public function deleteAllData(Team $team): void\n {\n Log::info('Deleting automated report and results for team', [\n 'team' => $team->getId(),\n ]);\n\n $reports = $this->getTeamReports($team);\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n Log::info('Deleting report', [\n 'team' => $team->getId(),\n 'report' => $report->getId(),\n ]);\n\n $this->deleteAllReportResults($report);\n\n $report->delete();\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Carbon\\CarbonImmutable;\nuse Carbon\\CarbonInterface;\nuse Carbon\\Exceptions\\InvalidFormatException;\nuse DateTime;\nuse DateTimeInterface;\nuse DateTimeZone;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\InputTypeEnum;\nuse Jiminny\\Component\\AskAnything\\AskAnythingPromptService;\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Contracts\\Repositories\\PlaybookCategoryRepository;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\ModelNotFoundException;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Partner;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Repositories\\GroupRepository;\nuse Jiminny\\Repositories\\SearchRepository;\nuse Jiminny\\Repositories\\StageRepository;\nuse Throwable;\n\nclass AutomatedReportsService\n{\n public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';\n public const string TYPE_ASK_JIMINNY = 'ask_jiminny';\n\n /**\n * Standard report types (used by kiosk for existing automated reports).\n */\n // @TODO this will add filter, however if we need to control feature by FF we need conditional logic\n public const array TYPES = [\n ['id' => 'exec_summary', 'name' => 'Exec Summary'],\n ['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],\n ['id' => 'product_feedback', 'name' => 'Product Feedback'],\n ['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],\n// ['id' => 'questions', 'name' => 'Questions'],\n// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],\n ];\n\n public const array ALL_TYPES = [\n ...self::TYPES,\n ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],\n ];\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\n /**\n * Frequencies for standard (non-Ask Jiminny) reports.\n */\n public const array FREQUENCIES = [\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n\n /**\n * Frequencies for Ask Jiminny reports.\n */\n public const array ASK_JIMINNY_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ];\n\n public const string MEDIA_TYPE_PDF = 'pdf';\n public const string MEDIA_TYPE_PODCAST = 'podcast';\n public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];\n public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];\n public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];\n public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];\n\n public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];\n public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];\n public const int SENT_REPORT_AT_HOURS = 5;\n public const string PDF_KEY = 'pdf';\n public const string AUDIO_KEY = 'audio';\n\n private const array ALL_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n private const string S3_DIR = 'reports';\n private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];\n private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];\n\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly GroupRepository $groupRepository,\n private readonly UserRepository $userRepository,\n private readonly StageRepository $stageRepository,\n private readonly DealStagesService $dealStagesService,\n private readonly RecipientsService $recipientsService,\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly Webhook $webhookService,\n private readonly BusDispatcher $dispatcher,\n private readonly ActivityTypeService $activityTypeService,\n private readonly PlaybookCategoryRepository $playbookCategoryRepository,\n private readonly AskAnythingPromptService $askAnythingPromptService,\n private readonly SearchRepository $activitySearchRepository,\n private readonly AskAnythingRepository $askAnythingRepository,\n ) {\n }\n\n public static function getTypes(): array\n {\n $types = self::TYPES;\n\n return array_map(static function ($type) {\n return $type['id'];\n }, $types);\n }\n\n public static function getCallTypes(): array\n {\n return array_map(static function ($callType) {\n return $callType['id'];\n }, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);\n }\n\n public static function getFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::FREQUENCIES);\n }\n\n // front-facing structure\n public function getReportEnabledFieldData(bool $value = false): array\n {\n return [\n 'id' => 'report_enabled',\n 'label' => '',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'value' => $value,\n ];\n }\n\n // Organizations = Teams\n public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array\n {\n $options = $this->getTeams(partner: $partner);\n\n if ($shortVersion) {\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value,\n 'dependencies' => [\n 'teams',\n 'deal_stage_at_call',\n 'current_deal_stage',\n 'recipients',\n ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,\n ],\n 'dependsOn' => [],\n ];\n }\n\n // Teams = Groups\n public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array\n {\n if ($shortVersion) {\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value, // value should be an array of objects {id, name}\n 'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],\n 'dependsOn' => [],\n ];\n }\n\n public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array\n {\n $types = [];\n if ($team instanceof Team) {\n if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n $types = self::TYPES;\n }\n if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {\n $types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];\n }\n } else {\n $types = self::TYPES;\n }\n\n if ($shortVersion) {\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'options' => $types,\n ];\n }\n\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $types,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getFrequencyFieldData(?string $value = null): array\n {\n return [\n 'id' => 'frequency',\n 'label' => 'Frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::FREQUENCIES,\n 'value' => $value,\n 'dependencies' => ['period'],\n 'dependsOn' => [],\n ];\n }\n\n public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array\n {\n return [\n 'id' => 'period',\n 'label' => 'Select one-off period',\n 'inputType' => InputTypeEnum::DATE_RANGE,\n 'required' => true,\n 'placeholder' => 'Select',\n 'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],\n 'queryParams' => [\n 'startDate' => 'start_date_period',\n 'endDate' => 'end_date_period',\n ],\n 'dependencies' => [],\n 'dependsOn' => ['frequency'],\n ];\n }\n\n public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array\n {\n return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);\n }\n\n public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);\n }\n\n public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);\n }\n\n public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'deal_value',\n 'label' => 'Deal Value',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_deal_value',\n 'max' => 'max_deal_value',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array\n {\n $value = [];\n $conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;\n $dialerOn && $value[] = self::CALL_TYPE_DIALER;\n\n return [\n 'id' => 'call_type',\n 'label' => 'Call Type',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => [\n self::CALL_TYPE_CONFERENCE,\n self::CALL_TYPE_DIALER,\n ],\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getMediaTypeFieldData(?AutomatedReport $report = null): array\n {\n $value = [];\n\n if ($report) {\n $value = $this->transformMediaTypes($report);\n }\n\n return [\n 'id' => 'media_types',\n 'label' => 'Export as',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => self::MEDIA_TYPE_OBJECTS,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'call_duration',\n 'label' => 'Call Duration',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_call_duration',\n 'max' => 'max_call_duration',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getRecipientsFieldData(?Team $team = null, array $value = []): array\n {\n return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);\n }\n\n public function getJiminnyRecipientsFieldData(array $value = []): array\n {\n return $this->recipientsService->getJiminnyRecipientsFieldData($value);\n }\n\n public function getAdditionalPromptInputFieldData(?string $value = null): array\n {\n return [\n 'id' => 'additional_prompt_input',\n 'label' => 'Special requirements',\n 'inputType' => InputTypeEnum::TEXTAREA,\n 'required' => false,\n 'placeholder' => 'What should be the focus of the report?',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCustomReportNameFieldData(?string $value = null): array\n {\n return [\n 'id' => 'custom_name',\n 'label' => 'Custom report name',\n 'inputType' => InputTypeEnum::TEXT,\n 'required' => false,\n 'placeholder' => 'Enter custom name',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n // data providers\n public function getTeams(?Partner $partner = null): array\n {\n $teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);\n\n $teamData = [];\n foreach ($teams as $team) {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n continue;\n }\n\n $teamData[] = $this->transformTeam($team);\n }\n\n return $teamData;\n }\n\n public function getTeamGroups(string $teamUuid): array\n {\n $data = [];\n $team = $this->getTeam($teamUuid);\n\n if ($team !== null) {\n $groups = $team->groups()->get();\n\n foreach ($groups as $group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array\n {\n $data = [];\n $teams = $this->getTeams(partner: $partner);\n\n foreach ($teams as $team) {\n if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {\n continue;\n }\n\n $data[] = [\n 'label' => $team['name'],\n 'groups' => $this->getTeamGroups($team['id']),\n ];\n }\n\n return $data;\n }\n\n public function getTeam(string $teamUuid): ?Team\n {\n return $this->teamRepository->idOrUuid($teamUuid);\n }\n\n public function getTeamById(int $teamId): ?Team\n {\n return $this->teamRepository->find($teamId);\n }\n\n public function getGroupsUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportGroups = $report->getGroups();\n foreach ($reportGroups as $groupId) {\n if ($group = $this->groupRepository->find($groupId)) {\n $uuids[] = $group->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getPlaybookCategoriesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $playbookCategories = $report->getPlaybookCategories();\n foreach ($playbookCategories as $id) {\n if ($category = $this->playbookCategoryRepository->find($id)) {\n $uuids[] = $category->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getDealAtCallStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getDealAtCallStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getCurrentDealStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getCurrentDealStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getRecipients());\n }\n\n public function getJiminnyUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function extractUserUuids(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => $user->getUuid())\n ->values()\n ->all();\n }\n\n // get mail data\n public function getRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getRecipients());\n }\n\n /**\n * @return array<UserContract>\n */\n public function getRecipientUserObjects(AutomatedReport $report): array\n {\n $userIds = $report->getRecipients()['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->values()\n ->all();\n }\n\n private function getJiminnyRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function buildRecipientUsers(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => [\n 'email' => $user->getEmailAddress(),\n 'name' => $user->getName(),\n 'timezone' => $user->getTimezone()->getName(),\n ])\n ->values()\n ->all();\n }\n\n public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array\n {\n if ($report->isAskJiminnyReport()) {\n $recipients = $this->resolveAskJiminnyRecipients($report);\n } else {\n $recipients = $this->getRecipientUsers($report);\n if ($includeJiminny) {\n $recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));\n }\n }\n\n $emails = [];\n\n return array_values(array_filter(\n $recipients,\n static function ($recipient) use (&$emails) {\n if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {\n return false;\n }\n $emails[] = $recipient['email'];\n\n return true;\n }\n ));\n }\n\n private function resolveAskJiminnyRecipients(AutomatedReport $report): array\n {\n $recipients = [];\n\n $creator = $report->getCreator();\n if ($creator !== null) {\n $recipients[] = [\n 'email' => $creator->getEmailAddress(),\n 'name' => $creator->getName(),\n 'timezone' => $creator->getTimezone()->getName(),\n ];\n }\n\n return array_merge(\n $recipients,\n $this->buildRecipientUsers($report->getRecipients()),\n $this->getGroupRecipientUsers($report),\n );\n }\n\n private function getGroupRecipientUsers(AutomatedReport $report): array\n {\n $users = [];\n foreach ($report->getGroups() as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group === null) {\n continue;\n }\n foreach ($group->getMembers() as $member) {\n $users[] = [\n 'email' => $member->getEmailAddress(),\n 'name' => $member->getName(),\n 'timezone' => $member->getTimezone()->getName(),\n ];\n }\n }\n\n return $users;\n }\n\n public function getReportTypeName(AutomatedReportResult $report): string\n {\n $type = $report->getReport()->getType();\n\n $getType = $this->transformReportType($type);\n\n return $getType['name'];\n }\n\n public function getReportPeriodName(AutomatedReportResult $report): string\n {\n $from = $report->getFromDate();\n $to = $report->getToDate();\n $frequency = $report->getReport()->getFrequency();\n\n if ($from === null || $to === null) {\n if (! $report->getReport()->isAskJiminnyReport()) {\n $invalidPeriod = $from === null ? 'from' : 'to';\n\n throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);\n }\n\n $timezone = $report->getReport()->getCreator()?->getTimezone();\n $period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);\n $from = $period['fromDate'];\n $to = $period['toDate'];\n }\n\n return $this->formatReportPeriodName($frequency, $from, $to);\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 getReportTeamsName(AutomatedReportResult $report): string\n {\n $groups = $report->getGroups();\n\n if (empty($groups)) {\n return 'All';\n }\n\n // Get group names from repository\n $groupNames = [];\n foreach ($groups as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group) {\n $groupNames[] = $group->getName();\n }\n }\n\n if (count($groupNames) === 1) {\n // Single team format\n $teamsName = $groupNames[0];\n } else {\n // Multiple teams format\n $teamsName = implode(', ', $groupNames);\n }\n\n return $teamsName;\n }\n\n public function getReportFileName(AutomatedReportResult $report): string\n {\n $customName = $report->getReport()->getCustomName();\n $periodName = $this->getReportPeriodName($report);\n $filenameSuffix = $this->getFilenameSuffix($report);\n\n if ($customName) {\n if ($filenameSuffix) {\n $customName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$customName} - {$periodName}\");\n }\n\n $baseName = $this->getReportTypeName($report);\n\n if ($filenameSuffix) {\n $baseName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}\");\n }\n\n public function getReportFileNameWithExtension(AutomatedReportResult $result): string\n {\n $extension = $this->getMediaTypeMetadata($result)['extension'];\n\n return $this->getReportFileName($result) . '.' . $extension;\n }\n\n public function sanitizeFileName(string $fileName): string\n {\n return str_replace(['/', '\\\\'], '-', $fileName);\n }\n\n public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool\n {\n $recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);\n if (in_array($user->getId(), $recipientIds, true)) {\n return true;\n }\n\n if ($report->isAskJiminnyReport()) {\n $groupId = $user->getGroupId();\n if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {\n return true;\n }\n }\n\n return false;\n }\n\n public function transformReportResults(Collection $automatedReportResults): array\n {\n $data = [];\n foreach ($automatedReportResults as $automatedReportResult) {\n /** @var AutomatedReportResult $automatedReportResult */\n\n $report = $automatedReportResult->getReport();\n\n $createdBy = $report->getCreator();\n $creator = [\n 'id' => $createdBy?->getUuid(),\n 'name' => $createdBy?->getName(),\n 'email' => $createdBy?->getEmailAddress(),\n 'photoUrl' => $createdBy?->getPhotoUrl(),\n ];\n\n $data[] = [\n 'id' => $automatedReportResult->getUuid(),\n 'name' => $automatedReportResult->getName(),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n 'recipients' => $this->buildRecipients($report),\n 'report_type' => $this->transformReportType($report->getType()),\n 'media_type' => $automatedReportResult->getMediaType(),\n 'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),\n 'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),\n 'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),\n 'creator' => $creator,\n ];\n }\n\n return $data;\n }\n\n private function buildRecipients(AutomatedReport $report): array\n {\n $creatorUuid = $report->getCreator()?->getUuid();\n\n $recipients = array_values(array_filter(\n $this->transformRecipients($report->getRecipients()),\n static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,\n ));\n\n if (! $report->isAskJiminnyReport()) {\n return $recipients;\n }\n\n return [\n ...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),\n ...$recipients,\n ];\n }\n\n public function hasCallTypeConference(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);\n }\n\n public function hasCallTypeDialer(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);\n }\n\n // transformers\n private function transformTeam(Team $team): array\n {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n return [];\n }\n\n return [\n 'id' => $team->getUuid(),\n 'name' => $team->getName(),\n ];\n }\n\n private function transformReportFullView(AutomatedReport $report): array\n {\n $base = $this->transformReportBase($report);\n\n return $report->getType() === self::TYPE_ASK_JIMINNY\n ? $base + $this->transformAskJiminnyFields($report)\n : $base + $this->transformStandardReportFields($report);\n }\n\n private function transformReportBase(AutomatedReport $report): array\n {\n return [\n 'id' => $report->getUuid(),\n 'organization' => $this->transformOrganization(team: $report->getTeam()),\n 'report_type' => $this->transformReportType($report->getType()),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n ];\n }\n\n private function transformStandardReportFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n\n return [\n 'report_enabled' => $report->getStatus(),\n 'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),\n 'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),\n 'deal_value_min' => $report->getDealValueMin(),\n 'deal_value_max' => $report->getDealValueMax(),\n 'call_types' => $this->transformCallType($report->getCallTypes()),\n 'media_types' => $this->transformMediaTypes($report),\n 'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),\n 'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),\n 'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),\n 'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),\n 'recipients' => $this->transformRecipients($report->getRecipients()),\n 'created_by' => $this->transformCreator($report->getCreator()),\n 'additional_prompt_input' => $report->getAdditionalPromptInput(),\n 'custom_name' => $report->getCustomName(),\n 'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),\n 'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),\n 'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),\n ];\n }\n\n private function transformAskJiminnyFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n $creatorId = $report->getAttribute('created_by');\n $explicitUserIds = array_values(array_filter(\n $report->getRecipients()['users'] ?? [],\n static fn ($id) => $id !== $creatorId\n ));\n\n return [\n 'report_name' => $report->getCustomName(),\n 'enabled' => $report->getStatus(),\n 'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),\n 'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),\n 'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),\n 'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),\n ];\n }\n\n private function transformOrganization(?Team $team): array\n {\n return [\n 'id' => $team?->getUuid(),\n 'name' => $team?->getName(),\n ];\n }\n\n private function transformReportType(string $type): array\n {\n foreach (self::ALL_TYPES as $typeItem) {\n if ($typeItem['id'] === $type) {\n return $typeItem;\n }\n }\n\n return [];\n }\n\n private function transformCallType(array $types): array\n {\n $result = [];\n $callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];\n\n foreach ($types as $type) {\n foreach ($callTypes as $callTypeItem) {\n if ($callTypeItem['id'] === $type) {\n $result[] = $callTypeItem;\n\n break;\n }\n }\n }\n\n return $result;\n }\n\n private function transformMediaTypes(AutomatedReport $report): array\n {\n $values = [];\n\n foreach ($report->getMediaTypes() as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n continue;\n }\n\n $values[] = match ($mediaType) {\n self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,\n self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,\n };\n }\n\n return $values;\n }\n\n private function transformFrequency(string $frequency): array\n {\n foreach (self::ALL_FREQUENCIES as $frequencyItem) {\n if ($frequencyItem['id'] === $frequency) {\n return $frequencyItem;\n }\n }\n\n return [];\n }\n\n public function transformDurationToMinutes(?int $duration): ?int\n {\n if (! $duration) {\n return null;\n }\n\n return (int) ($duration / 60);\n }\n\n private function transformGroups(?Team $team, array $groupsIds): array\n {\n if (empty($groupsIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($groupsIds as $groupId) {\n $group = $team->groups()->where('id', $groupId)->first();\n\n if ($group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n 'photoUrl' => $group->getPhotoUrl(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformStages(?Team $team, array $stagesIds): array\n {\n if (empty($stagesIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($stagesIds as $stageId) {\n $stage = $team->stages()->where('id', $stageId)->first();\n\n if ($stage) {\n $data[] = [\n 'id' => $stage->getUuid(),\n 'name' => $stage->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformRecipients(array $recipients): array\n {\n $users = [];\n foreach ($recipients['users'] ?? [] as $userId) {\n $users[] = $this->transformUser($userId);\n }\n\n return $users;\n }\n\n private function transformCreator(?User $user): ?array\n {\n if ($user === null) {\n return null;\n }\n\n return $this->transformUser($user->getId());\n }\n\n private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array\n {\n if ($prompt === null) {\n return null;\n }\n\n return [\n 'id' => $prompt->getUuid(),\n 'name' => $prompt->getTitle(),\n ];\n }\n\n private function transformSafeSearch(?Search $search): ?array\n {\n if ($search === null) {\n return null;\n }\n\n return [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ];\n }\n\n private function transformUser(int $userId): array\n {\n /* @var ?User $user */\n $user = $this->userRepository->find($userId);\n\n return [\n 'id' => $user?->getUuid(),\n 'name' => $user?->getName(),\n 'email' => $user?->getEmailAddress(),\n 'photoUrl' => $user?->getPhotoUrl(),\n ];\n }\n\n public function create(array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $validatedData['created_by'] = auth()->id();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function update(string $uuid, array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $report = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $report) {\n throw new InvalidArgumentException('Report not found');\n }\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Create an Ask Jiminny report.\n */\n public function createAskJiminnyReport(array $data, User $creator): array\n {\n $validatedData = $this->validateAskJiminnyReportData($data, $creator);\n $validatedData['created_by'] = $creator->getId();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Update an Ask Jiminny report.\n */\n public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array\n {\n if (! $report->isAskJiminnyReport()) {\n throw new InvalidArgumentException('Report is not an Ask Jiminny report');\n }\n\n $validatedData = $this->validateAskJiminnyReportData($data, $user);\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array\n {\n if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {\n throw new InvalidArgumentException(\n 'This report is missing a saved search or prompt. ' .\n 'Edit the report to complete the setup before enabling it.'\n );\n }\n\n $this->automatedReportsRepository->update($report, ['status' => $status]);\n\n return $this->transformReportFullView($report->fresh());\n }\n\n /**\n * Validate and transform data for Ask Jiminny reports.\n */\n private function validateAskJiminnyReportData(array $data, User $user): array\n {\n // Validate name\n $name = trim($data['report_name'] ?? '');\n if (empty($name)) {\n throw new InvalidArgumentException('Report name is required');\n }\n if (mb_strlen($name) > 50) {\n throw new InvalidArgumentException('Report name must be 50 characters or less');\n }\n\n // Validate frequency (only daily, weekly, monthly for Ask Jiminny)\n $frequency = $data['frequency'] ?? null;\n $askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];\n if (! in_array($frequency, $askJiminnyFrequencies, true)) {\n throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');\n }\n\n // Validate expiration date\n $expiresAt = $data['expires_on'] ?? null;\n if (empty($expiresAt)) {\n throw new InvalidArgumentException('Expiration date is required');\n }\n\n try {\n $expiresAtDate = Carbon::parse($expiresAt);\n } catch (InvalidFormatException $e) {\n throw new InvalidArgumentException('Expiration date format is invalid');\n }\n $maxExpiration = Carbon::now()->addYear()->endOfDay();\n if ($expiresAtDate->gt($maxExpiration)) {\n throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');\n }\n if ($expiresAtDate->isPast()) {\n throw new InvalidArgumentException('Expiration date cannot be in the past');\n }\n\n // Validate saved search\n $activitySearchId = $data['saved_search'] ?? null;\n if (empty($activitySearchId)) {\n throw new InvalidArgumentException('Saved search is required');\n }\n $savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);\n if (! $savedSearch) {\n throw new InvalidArgumentException('Saved search not found or does not belong to you');\n }\n\n // Validate saved prompt\n $askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;\n if (empty($askAnythingPromptId)) {\n throw new InvalidArgumentException('Ask Jiminny prompt is required');\n }\n $prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);\n if (! $prompt) {\n throw new InvalidArgumentException('Ask Jiminny prompt not found');\n }\n\n // Validate status\n $status = $data['enabled'] ?? false;\n\n $recipientUserIds = [$user->getId()];\n\n if (! empty($data['share_users'])) {\n $sharedUserIds = $this->validateAndGetUserIdsByTeam(\n $user->team,\n (array) $data['share_users']\n );\n $recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);\n }\n\n $sharedGroupIds = [];\n if (! empty($data['share_teams'])) {\n $sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);\n }\n\n $recipientUserIds = array_values(array_unique($recipientUserIds));\n\n return [\n 'team_id' => $user->getTeamId(),\n 'type' => self::TYPE_ASK_JIMINNY,\n 'status' => (bool) $status,\n 'frequency' => $frequency,\n 'custom_name' => $name,\n 'activity_search_id' => $savedSearch->getId(),\n 'ask_anything_prompt_id' => $prompt->getId(),\n 'expires_at' => $expiresAtDate->toDateString(),\n 'media_types' => [self::MEDIA_TYPE_PDF],\n 'call_types' => [],\n 'recipients' => ['users' => $recipientUserIds],\n 'groups' => $sharedGroupIds,\n ];\n }\n\n public static function getAskJiminnyFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::ASK_JIMINNY_FREQUENCIES);\n }\n\n public function getAskJiminnyReportFilters(User $user): array\n {\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n return [\n [\n 'id' => 'prompt',\n 'label' => 'Prompt',\n 'options' => $prompts,\n ],\n [\n 'id' => 'saved_search',\n 'label' => 'Saved Search',\n 'options' => $savedSearches,\n ],\n ];\n }\n\n public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array\n {\n $team = $user->getTeam();\n $userTimezone = $user->getTimezone();\n\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n $teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ])->values()->all();\n\n $shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];\n\n $sharedTeamsValue = [];\n $sharedUsersValue = [];\n if ($report) {\n $sharedTeamsValue = $this->transformGroups($team, $report->getGroups());\n\n $recipientUserIds = $report->getRecipients()['users'] ?? [];\n $creatorId = $report->getAttribute('created_by');\n $sharedUserIds = array_values(array_filter(\n $recipientUserIds,\n static fn ($id) => $id !== $creatorId\n ));\n $sharedUsersValue = collect($sharedUserIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (User $u) => [\n 'id' => $u->getUuid(),\n 'name' => $u->getName(),\n ])\n ->values()\n ->all();\n }\n\n return [\n 'fields' => [\n [\n 'id' => 'enabled',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'label' => '',\n 'value' => $report?->getStatus() ?? false,\n ],\n [\n 'id' => 'report_name',\n 'inputType' => InputTypeEnum::TEXT,\n 'label' => 'Name',\n 'placeholder' => 'Enter name',\n 'required' => true,\n 'validation' => ['maxLength' => 50],\n 'value' => $report?->getCustomName() ?? '',\n ],\n [\n 'id' => 'frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Frequency',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::ASK_JIMINNY_FREQUENCIES,\n 'value' => $report ? $this->transformFrequency($report->getFrequency()) : null,\n ],\n [\n 'id' => 'expires_on',\n 'inputType' => InputTypeEnum::DATE,\n 'label' => 'Expires on',\n 'required' => true,\n 'placeholder' => 'Select',\n 'validation' => [\n 'minDate' => now($userTimezone)->toDateString(),\n 'maxDate' => now($userTimezone)->addYear()->toDateString(),\n ],\n 'value' => $report?->getExpiresAt()?->toDateString(),\n ],\n [\n 'id' => 'share_teams',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team',\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $teamGroups,\n 'value' => $sharedTeamsValue,\n ],\n [\n 'id' => 'share_users',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team member',\n 'required' => false,\n 'placeholder' => 'Select',\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'options' => $shareUsers,\n 'value' => $sharedUsersValue,\n ],\n [\n 'id' => 'saved_search',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Saved search',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $savedSearches,\n 'value' => $report && $report->getSavedSearch() ? [\n 'id' => $report->getSavedSearch()->getUuid(),\n 'name' => $report->getSavedSearch()->getName(),\n ] : null,\n ],\n [\n 'id' => 'ask_jiminny_prompt',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Ask Jiminny prompt',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $prompts,\n 'value' => $report && $report->getAskAnythingPrompt() ? [\n 'id' => $report->getAskAnythingPrompt()->getUuid(),\n 'name' => $report->getAskAnythingPrompt()->getTitle(),\n ] : null,\n ],\n ],\n ];\n }\n\n private function updateResultNames(AutomatedReport $automatedReport): void\n {\n $results = $this->automatedReportsRepository->getResultsByReport($automatedReport);\n\n foreach ($results as $result) {\n $result->update(['name' => $this->getReportFileName($result)]);\n }\n }\n\n public function updateStatus(string $uuid, array $data): array\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $automatedReport->update([\n 'status' => $status,\n ]);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n private function generateOneOffReport(AutomatedReport $automatedReport): void\n {\n // the scheduler handles all the other frequency types\n if ($automatedReport->getStatus() === false || $automatedReport->getFrequency() !== self::FREQUENCY_ONE_OFF) {\n return;\n }\n\n $this->dispatcher->dispatch(new RequestGenerateReportJob($automatedReport->getUuid()));\n }\n\n public function getReport(string $uuid, ?Partner $partner = null): AutomatedReport\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n if ($partner !== null && ! $partner->isDefaultPartner() && $automatedReport->team->partner_id !== $partner->getId()) {\n throw new ModelNotFoundException('Report not found');\n }\n\n return $automatedReport;\n }\n\n public function get(string $uuid, ?Partner $partner = null): array\n {\n $automatedReport = $this->getReport($uuid, $partner);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function list(string $sortColumn = 'created_at', string $sortDirection = 'desc', ?Partner $partner = null): array\n {\n $results = [];\n $collection = $this->automatedReportsRepository->getAllStandardReports($sortColumn, $sortDirection, $partner);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function listAskJiminnyReports(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): array {\n $results = [];\n $collection = $this->automatedReportsRepository->getAskJiminnyReportsByUser($user, $sortColumn, $sortDirection);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function delete(string $uuid): void\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $automatedReport->delete();\n }\n\n public function createReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n return $this->automatedReportsRepository->createResult(\n array_merge(\n [\n 'report_id' => $automatedReport->getId(),\n 'status' => AutomatedReportResult::STATUS_DEFAULT,\n ],\n $data\n )\n );\n }\n\n public function getOrCreateReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n $existing = $this->automatedReportsRepository->findLatestSameDayDefaultOrFailedResult($automatedReport);\n\n if ($existing !== null) {\n $existing->update(['status' => AutomatedReportResult::STATUS_DEFAULT]);\n\n return $existing;\n }\n\n return $this->createReportResult($automatedReport, $data);\n }\n\n public function getReportResult(string $resultUuid): AutomatedReportResult\n {\n $report = $this->automatedReportsRepository->findResultByUuid($resultUuid);\n\n if (! $report) {\n throw new ModelNotFoundException('Report Result not found');\n }\n\n return $report;\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return $this->automatedReportsRepository->findChildResult($result, $type);\n }\n\n // prophet API calls\n /**\n * @throws ApplicationException\n */\n public function getGenerateReportPayload(AutomatedReport $automatedReport, string $reportResultUuid): array\n {\n $period = $this->calculateFromAndToDate($automatedReport);\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n return [\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResultUuid,\n 'report_type' => $automatedReport->getType(),\n 'media_types' => $automatedReport->getMediaTypes(),\n 'from_date' => $fromDate->startOfDay()->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->endOfDay()->format(DateTimeInterface::RFC3339),\n 'group_ids' => $automatedReport->getGroups(),\n 'call_deal_stage' => $automatedReport->getDealAtCallStages(),\n 'current_deal_stage' => $automatedReport->getCurrentDealStages(),\n 'deal_min_value' => $automatedReport->getDealValueMin(),\n 'deal_max_value' => $automatedReport->getDealValueMax(),\n 'call_types' => $automatedReport->getCallTypes(),\n 'call_duration_min_seconds' => $automatedReport->getCallDurationMin(),\n 'call_duration_max_seconds' => $automatedReport->getCallDurationMax(),\n 'special_requirements' => $automatedReport->getAdditionalPromptInput(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->formatReportPeriodName(\n $automatedReport->getFrequency(),\n $fromDate,\n $toDate,\n ),\n 'playbook_categories' => $automatedReport->getPlaybookCategories(),\n 'custom_name' => $automatedReport->getCustomName(),\n ];\n }\n\n // $inputPayload - FE payload structure\n public function getActivitiesCountPayload(array $inputPayload): array\n {\n // Use validateAndTransformData to validate and normalize input\n $validatedData = $this->validateAndTransformData($inputPayload);\n $period = $this->calculateFromAndToDatePeriod(\n $validatedData['frequency'],\n Carbon::parse($validatedData['from']),\n Carbon::parse($validatedData['to']),\n );\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n // Create payload similar to getGenerateReportPayload\n return [\n 'team_id' => $validatedData['team_id'],\n 'group_ids' => $validatedData['groups'] ?? [],\n 'report_type' => $validatedData['type'],\n 'from_date' => $fromDate->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->format(DateTimeInterface::RFC3339),\n 'call_deal_stage' => $validatedData['deal_at_call_stages'] ?? [],\n 'current_deal_stage' => $validatedData['current_deal_stages'] ?? [],\n 'deal_min_value' => $validatedData['deal_value_min'] ?? null,\n 'deal_max_value' => $validatedData['deal_value_max'] ?? null,\n 'call_types' => $validatedData['call_types'],\n 'call_duration_min_seconds' => $validatedData['call_duration_min'] ?? null,\n 'call_duration_max_seconds' => $validatedData['call_duration_max'] ?? null,\n 'special_requirements' => $validatedData['additional_prompt_input'] ?? null,\n 'playbook_categories' => $validatedData['playbook_categories'] ?? [],\n 'request_id' => null,\n 'callback_url' => null,\n ];\n }\n\n public function shouldSendReport(array $users, ?CarbonInterface $generatedAt = null): bool\n {\n if (empty($users)) {\n return false;\n }\n\n $earliestTz = collect($users)\n ->mapWithKeys(function (array $user) {\n $tz = new DateTimeZone($user['timezone']);\n $nowUtc = new DateTime('now', new DateTimeZone('UTC'));\n $offset = $tz->getOffset($nowUtc);\n\n return [$user['timezone'] => $offset];\n })\n ->sortDesc()\n ->keys()\n ->first();\n\n $now = Carbon::now($earliestTz);\n $isScheduledTime = (int) $now->format('H') === self::SENT_REPORT_AT_HOURS;\n\n if ($isScheduledTime) {\n return true;\n }\n\n return $this->hasPassedScheduledTime($generatedAt, $earliestTz);\n }\n\n public function hasPassedScheduledTime(?CarbonInterface $generatedAt, string $timezone): bool\n {\n if ($generatedAt === null) {\n return false;\n }\n\n $now = Carbon::now($timezone);\n $scheduledTime = $now->copy()->setTime(self::SENT_REPORT_AT_HOURS, 0, 0);\n\n if ($now->hour < self::SENT_REPORT_AT_HOURS) {\n $scheduledTime = $scheduledTime->subDay();\n }\n\n $scheduledTimeUtc = $scheduledTime->copy()->utc();\n $generatedAtUtc = $generatedAt->copy()->utc();\n $nowUtc = $now->copy()->utc();\n\n return $generatedAtUtc->lt($scheduledTimeUtc) && $nowUtc->gt($scheduledTimeUtc);\n }\n\n public function calculateFromAndToDatePeriod(\n string $frequency,\n ?Carbon $fromDate = null,\n ?Carbon $toDate = null,\n DateTimeZone|string|null $timezone = 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($timezone);\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()->subWeek()->startOfWeek(CarbonInterface::MONDAY),\n 'toDate' => $now->copy()->subWeek()->endOfWeek(CarbonInterface::SUNDAY),\n ],\n self::FREQUENCY_MONTHLY => [\n 'fromDate' => $now->copy()->subMonthNoOverflow()->startOfMonth(),\n 'toDate' => $now->copy()->subMonthNoOverflow()->endOfMonth(),\n ],\n self::FREQUENCY_QUARTERLY => [\n 'fromDate' => $now->copy()->subQuarterNoOverflow()->startOfQuarter(),\n 'toDate' => $now->copy()->subQuarterNoOverflow()->endOfQuarter(),\n ],\n default => throw new InvalidArgumentException(\"Unsupported frequency: {$frequency}\"),\n };\n }\n\n private function calculateFromAndToDate(AutomatedReport $automatedReport): array\n {\n return $this->calculateFromAndToDatePeriod(\n $automatedReport->getFrequency(),\n $automatedReport->getFrom(),\n $automatedReport->getTo()\n );\n }\n\n public function getAskJiminnyGenerateReportPayload(\n AutomatedReport $automatedReport,\n AutomatedReportResult $reportResult,\n array $activityIds,\n ): array {\n return [\n 'user_question' => $automatedReport->getAskAnythingPrompt()?->getContent(),\n 'call_ids' => array_map('strval', $activityIds),\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResult->getUuid(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->getReportPeriodName($reportResult),\n 'report_name' => $automatedReport->getCustomName(),\n ];\n }\n\n private function getCallbackUrl(): string\n {\n return $this->webhookService->route('jiminny.webhook.reports.ready');\n }\n\n /**\n * Validate and transform payload data for automated reports\n *\n * @param array $data\n *\n * @throws InvalidArgumentException\n *\n * @return array\n */\n private function validateAndTransformData(array $data): array\n {\n // Validate organization (team) and check feature\n $team = $this->validateOrganization($data['organization'] ?? null);\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $type = $this->validateReportType($data['report_type'] ?? null);\n $frequency = $this->validateFrequency($data['frequency'] ?? null);\n $additionalPromptInput = $this->validateAdditionalPromptInput(\n $data['additional_prompt_input'] ?? null\n );\n $customReportName = $this->validateCustomReportName($data['custom_name'] ?? null);\n\n // Prepare data for the database\n $reportData = [\n 'team_id' => $team->getId(),\n 'type' => $type,\n 'status' => $status,\n 'frequency' => $frequency,\n 'additional_prompt_input' => $additionalPromptInput,\n 'custom_name' => $customReportName,\n ];\n\n // Validate deal values\n $reportData = $this->validateDealValues($data, $reportData);\n\n // Validate date range\n $reportData = $this->validateDateRange($data, $reportData, $frequency);\n\n // Validate call durations\n $reportData = $this->validateCallDurations($data, $reportData);\n\n // Validate call types\n $reportData = $this->validateCallTypes($data, $reportData);\n\n // Validate media types\n $reportData = $this->validateMediaTypes($data, $reportData);\n\n // Validate groups\n if (isset($data['teams'])) {\n $reportData['groups'] = $this->validateAndGetGroupIds($team, $data['teams']);\n }\n\n // Validate deal stages\n $reportData = $this->validateDealStages($data, $reportData, $team, $type);\n\n // Validate playbook categories\n $reportData = $this->validatePlaybookCategories($data, $reportData, $team);\n\n // Validate recipients\n $reportData['recipients'] = [\n 'users' => $this->validateAndGetUserIdsByTeam($team, $data['recipients'] ?? []),\n ];\n\n if (isset($data['jiminny_recipients'])) {\n // Validate Jiminny recipients\n $reportData['jiminny_recipients'] = [\n 'users' => $this->validateAndGetJiminnyUserIds((array) $data['jiminny_recipients']),\n ];\n }\n\n return $reportData;\n }\n\n private function validateDealValues(array $data, array $reportData): array\n {\n if (isset($data['min_deal_value'])) {\n $reportData['deal_value_min'] = (int) $data['min_deal_value'];\n\n if ($reportData['deal_value_min'] > 4294967295 || $reportData['deal_value_min'] < 0) {\n throw new InvalidArgumentException('Min deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_deal_value'])) {\n $reportData['deal_value_max'] = (int) $data['max_deal_value'];\n\n if ($reportData['deal_value_max'] > 4294967295 || $reportData['deal_value_max'] < 0) {\n throw new InvalidArgumentException('Max deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['min_deal_value'], $data['max_deal_value'])\n && $data['min_deal_value'] > $data['max_deal_value']\n ) {\n throw new InvalidArgumentException('Min deal value cannot be greater than max deal value');\n }\n\n return $reportData;\n }\n\n private function validateDateRange(array $data, array $reportData, string $frequency): array\n {\n // Set date range only for one_off frequency\n if ($frequency === 'one_off') {\n if (isset($data['start_date_period'])) {\n $reportData['from'] = $this->parseDate($data['start_date_period']);\n }\n\n if (isset($data['end_date_period'])) {\n $reportData['to'] = $this->parseDate($data['end_date_period']);\n }\n\n if (empty($reportData['from']) || empty($reportData['to'])) {\n throw new InvalidArgumentException(\n 'Start date and end date are required for one_off frequency'\n );\n }\n } else {\n $reportData['from'] = null;\n $reportData['to'] = null;\n }\n\n return $reportData;\n }\n\n private function validateCallDurations(array $data, array $reportData): array\n {\n // Convert call durations from minutes to seconds\n if (isset($data['min_call_duration'])) {\n $reportData['call_duration_min'] = (int) $data['min_call_duration'] * 60;\n\n if ($reportData['call_duration_min'] > 4294967295 || $reportData['call_duration_min'] < 0) {\n throw new InvalidArgumentException('Min call duration should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_call_duration'])) {\n $reportData['call_duration_max'] = (int) $data['max_call_duration'] * 60;\n\n if ($reportData['call_duration_max'] > 4294967295 || $reportData['call_duration_max'] < 0) {\n throw new InvalidArgumentException('Max call duration should be between 0 and 4294967295');\n }\n }\n\n return $reportData;\n }\n\n private function validateCallTypes(array $data, array $reportData): array\n {\n // Set call types\n $reportData['call_types'] = $data['call_type'] ?? [];\n if (empty($reportData['call_types'])) {\n $reportData['call_types'] = self::getCallTypes();\n }\n\n foreach ($reportData['call_types'] as $callType) {\n if (! in_array($callType, self::getCallTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Call type %s is invalid', $callType));\n }\n }\n\n return $reportData;\n }\n\n private function validateMediaTypes(array $data, array $reportData): array\n {\n // Set media types from input data\n $reportData['media_types'] = $data['media_types'] ?? [];\n\n if (empty($reportData['media_types'])) {\n throw new InvalidArgumentException('Media types are required');\n }\n\n foreach ($reportData['media_types'] as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n throw new InvalidArgumentException(sprintf('Media type %s is invalid', $mediaType));\n }\n }\n\n return $reportData;\n }\n\n private function validateDealStages(array $data, array $reportData, Team $team, string $reportType): array\n {\n // Validate and set deal stages\n if (isset($data['deal_stage_at_call'])) {\n $reportData['deal_at_call_stages'] =\n $this->validateAndGetDealStageIds($team, $data['deal_stage_at_call'], 'Deal stage at call');\n }\n\n if (isset($data['current_deal_stage'])) {\n $reportData['current_deal_stages'] =\n $this->validateAndGetDealStageIds($team, $data['current_deal_stage'], 'Current deal stage');\n }\n\n // Ensure current_deal_stage is not provided for loss_analysis report type\n if ($reportType === self::TYPE_LOSS_ANALYSIS && ! empty($data['current_deal_stage'])) {\n throw new InvalidArgumentException('Current deal stage is not applicable for Loss Analysis reports');\n }\n\n return $reportData;\n }\n\n // transform uuid to id\n private function validatePlaybookCategories(array $data, array $reportData, Team $team): array\n {\n $key = 'playbook_categories';\n\n if (isset($data[$key])) {\n $payloadIds = $data[$key];\n $ids = [];\n\n foreach ($payloadIds as $uuid) {\n $uuid = (string) $uuid;\n\n try {\n $playbookCategory = $this->playbookCategoryRepository->findByUuid($uuid);\n } catch (Throwable $throwable) {\n Log::error(__METHOD__ . ' ' . $throwable->getMessage());\n\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory) {\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory->hasPlaybook()) {\n throw new InvalidArgumentException(sprintf('Playbook category %s has no playbook', $uuid));\n }\n\n if ($playbookCategory->getPlaybook()->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Playbook category %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $ids[] = $playbookCategory->getId();\n }\n\n $reportData[$key] = $ids;\n }\n\n return $reportData;\n }\n\n private function validateReportStatus($status): bool\n {\n if (! in_array($status, [true, false], true)) {\n throw new InvalidArgumentException('Report status is invalid');\n }\n\n return $status;\n }\n\n private function validateReportType($type): string\n {\n if (! in_array($type, self::getTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Report type is invalid: %s', $type));\n }\n\n return $type;\n }\n\n private function validateFrequency($frequency): string\n {\n if (! in_array($frequency, self::getFrequencies(), true)) {\n throw new InvalidArgumentException('Frequency is invalid');\n }\n\n return $frequency;\n }\n\n private function validateAdditionalPromptInput(?string $additionalPromptInput): ?string\n {\n if ($additionalPromptInput && strlen($additionalPromptInput) > 5000) {\n throw new InvalidArgumentException('Additional Prompt Input should be less than 5000 characters');\n }\n\n return $additionalPromptInput;\n }\n\n private function validateCustomReportName(?string $customReportName): ?string\n {\n if ($customReportName === null || $customReportName === '') {\n return null;\n }\n\n if (strlen($customReportName) > 70) {\n throw new InvalidArgumentException('Custom report name should be less than 70 characters');\n }\n\n return $customReportName;\n }\n\n private function validateOrganization(?string $organizationUuid): Team\n {\n if (! $organizationUuid) {\n throw new InvalidArgumentException('Organization is required');\n }\n\n $team = $this->teamRepository->idOrUuid($organizationUuid);\n\n if (! $team) {\n throw new InvalidArgumentException('Organization not found');\n }\n\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n throw new InvalidArgumentException('Organization does not have the Automated Reports feature');\n }\n\n return $team;\n }\n\n private function validateAndGetGroupIds(Team $team, array $teamUuids): array\n {\n $groupIds = [];\n\n foreach ($teamUuids as $uuid) {\n $group = $this->groupRepository->findByUuid($uuid);\n\n if ($group === null || $group->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Group %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $groupIds[] = $group->getId();\n\n }\n\n return $groupIds;\n }\n\n private function validateAndGetDealStageIds(Team $team, array $stageUuids, string $propertyLabel): array\n {\n $stageIds = [];\n\n foreach ($stageUuids as $uuid) {\n $stage = $this->stageRepository->findByUuid($uuid);\n\n if ($stage === null || $stage->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Stage %s not found for team %s for %s', $uuid, $team->getUuid(), $propertyLabel)\n );\n }\n\n $stageIds[] = $stage->getId();\n }\n\n return $stageIds;\n }\n\n private function validateAndGetUserIds(array $userUuids, callable $teamCheck): array\n {\n if (empty($userUuids)) {\n return [];\n }\n\n $userIds = [];\n\n foreach ($userUuids as $uuid) {\n $user = $this->userRepository->findByUuid($uuid);\n\n if (! $user || ! $user->isStatusActive()) {\n throw new InvalidArgumentException(\n sprintf('User %s not found or is not active', $uuid)\n );\n }\n\n if (! $teamCheck($user)) {\n throw new InvalidArgumentException(\n sprintf('User %s does not belong to the allowed team(s)', $uuid)\n );\n }\n\n $userIds[] = $user->getId();\n }\n\n return $userIds;\n }\n\n private function validateAndGetUserIdsByTeam(Team $team, array $userUuids): array\n {\n return $this->validateAndGetUserIds($userUuids, fn ($user) => $user->getTeamId() === $team->getId());\n }\n\n private function validateAndGetJiminnyUserIds(array $userUuids): array\n {\n $allowedTeamIds = config('kiosk.teamIds', []);\n\n return $this->validateAndGetUserIds($userUuids, fn ($user) => in_array($user->getTeamId(), $allowedTeamIds, true));\n }\n\n private function parseDate(string $dateString): string\n {\n return date('Y-m-d H:i:s', strtotime($dateString));\n }\n\n private function generateReportResultViewUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.view', ['uuid' => $result->getUuid()]);\n }\n\n private function generateReportResultDownloadUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.download', ['uuid' => $result->getUuid()]);\n }\n\n private function getReportMediaRouteResource(AutomatedReportResult $result): string\n {\n if ($result->getMediaType() === self::MEDIA_TYPE_PDF) {\n return self::PDF_KEY;\n } elseif ($result->getMediaType() === self::MEDIA_TYPE_PODCAST) {\n return self::AUDIO_KEY;\n }\n\n throw new \\InvalidArgumentException('Unknown media type.');\n }\n\n public function getMediaPath(AutomatedReportResult $result): ?string\n {\n $url = match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => $result->getPdfUrl(),\n self::MEDIA_TYPE_PODCAST => $result->getPodcastAudioUrl(),\n default => null,\n };\n\n if ($url === null) {\n return null;\n }\n\n $path = parse_url(trim($url, '\"\\''), PHP_URL_PATH);\n\n return $path ?: null;\n }\n\n public function getFilenameSuffix(AutomatedReportResult $result): ?string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => 'Podcast',\n default => null,\n };\n }\n\n public function getMailSubjectSuffix(AutomatedReportResult $result): string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => 'report',\n self::MEDIA_TYPE_PODCAST => 'podcast',\n default => '',\n };\n }\n\n public function getMediaTypeMetadata(AutomatedReportResult $result): array\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => ['extension' => 'mp3', 'mime' => 'audio/mpeg'],\n self::MEDIA_TYPE_PDF => ['extension' => 'pdf', 'mime' => 'application/pdf'],\n default => ['extension' => null, 'mime' => null],\n };\n }\n\n public function deleteS3Files(AutomatedReportResult $result): void\n {\n $teamUuid = $result->getReport()->getTeam()->getUuid();\n $reportUuid = $result->getUuid();\n\n // delete all files for a report uuid no mather of pdf, podcast, or both\n // in case of both - the podcast files are linked to the pdf (parent) uuid\n // pdf and podcast date times should be close\n $path = sprintf('%s/%s/%s', $teamUuid, self::S3_DIR, $reportUuid);\n\n foreach (self::FILE_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted S3 file', [\n 'path' => $file,\n ]);\n }\n }\n\n foreach (self::FILE_PODCAST_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '_podcast.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted Podcast S3 file', [\n 'path' => $file,\n ]);\n }\n }\n }\n\n /**\n *\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): Collection\n {\n return $this->automatedReportsRepository->getTeamIdsWithReportsResults($teamId);\n }\n\n /**\n * Core delete logic for report results using a query\n *\n * @param Builder $query\n * @param array $logContext\n *\n * @return int\n */\n private function deleteReportResultsByQuery(Builder $query, array $logContext = []): int\n {\n $deletedCount = 0;\n\n if ($query->exists()) {\n Log::info(\n 'Run delete report results',\n array_merge(\n $logContext,\n [\n 'service' => 'AutomatedReportsService',\n ]\n )\n );\n\n $query->chunkById(50, function ($results) use (&$deletedCount, $logContext) {\n foreach ($results as $result) {\n $this->deleteReportResult($result);\n $deletedCount++;\n\n Log::info(\n 'Deleted a report result',\n array_merge(\n $logContext,\n [\n 'result_id' => $result->getId(),\n 'report_id' => $result->getReportId(),\n ]\n )\n );\n }\n });\n }\n\n return $deletedCount;\n }\n\n /**\n * Delete report results for a team by retention period\n *\n * @param Team $team\n * @param CarbonImmutable $retentionDate\n *\n * @return int Number of deleted report results\n */\n public function deleteReportsResultsInRetentionPeriod(Team $team, CarbonImmutable $retentionDate): int\n {\n $reportIds = $this->automatedReportsRepository->getReportIdsByTeam($team);\n\n if ($reportIds->isEmpty()) {\n return 0;\n }\n\n $query = $this->automatedReportsRepository\n ->getReportResultsQueryForRetention($team, $retentionDate);\n\n return $this->deleteReportResultsByQuery($query, [\n 'team_id' => $team->getId(),\n 'retention_date' => $retentionDate->toDateTimeString(),\n ]);\n }\n\n /**\n * Delete ALL report results for a specific automated report\n *\n * @param string $uuid\n *\n * @return int\n */\n public function deleteReportResults(string $uuid): int\n {\n $report = $this->getReport($uuid);\n\n $query = $this->automatedReportsRepository->getResultsByReportQuery($report);\n\n return $this->deleteReportResultsByQuery($query, [\n 'report_uuid' => $uuid,\n 'report_id' => $report->getId(),\n ]);\n }\n\n public function deleteReportResult(AutomatedReportResult $result): void\n {\n $this->deleteS3Files($result);\n\n $result->delete();\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getTeamReports(Team $team): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getReportsByTeam($team);\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getReportResults(AutomatedReport $report): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getResultsByReport($report);\n }\n\n public function deleteAllReportResults(AutomatedReport $report): void\n {\n $results = $this->getReportResults($report);\n\n /** @var AutomatedReportResult $result */\n foreach ($results as $result) {\n Log::info('Deleting result', [\n 'report' => $report->getId(),\n 'result' => $result->getId(),\n ]);\n\n $this->deleteReportResult($result);\n }\n }\n\n public function deleteAllData(Team $team): void\n {\n Log::info('Deleting automated report and results for team', [\n 'team' => $team->getId(),\n ]);\n\n $reports = $this->getTeamReports($team);\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n Log::info('Deleting report', [\n 'team' => $team->getId(),\n 'report' => $report->getId(),\n ]);\n\n $this->deleteAllReportResults($report);\n\n $report->delete();\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.42785904,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.43650267,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.4474734,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.45611703,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.46476063,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.47573137,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.4867021,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"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.51329786,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.5242686,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.70611703,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"21","depth":4,"bounds":{"left":0.66921544,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.68085104,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"18","depth":4,"bounds":{"left":0.69015956,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.7017952,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.7117686,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.7287234,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"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 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 * 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 `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\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;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","depth":4,"on_screen":true,"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 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 * 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 `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\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;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Socket fail to connect to host:address=(host=localhost)(port=3306)(type=primary). Connection refused","depth":3,"bounds":{"left":0.42652926,"top":0.9577015,"width":0.29321808,"height":0.013567438},"on_screen":true,"value":"Socket fail to connect to host:address=(host=localhost)(port=3306)(type=primary). Connection refused","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1751808696220340334
|
1126710648141684156
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskAnythingPromptServiceTest
Run 'AskAnythingPromptServiceTest'
Debug 'AskAnythingPromptServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
100
3
34
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Kiosk\AutomatedReports;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Carbon\Exceptions\InvalidFormatException;
use DateTime;
use DateTimeInterface;
use DateTimeZone;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\ActivitySearch\FilterDefinition\InputTypeEnum;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Partner;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Throwable;
class AutomatedReportsService
{
public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';
public const string TYPE_ASK_JIMINNY = 'ask_jiminny';
/**
* Standard report types (used by kiosk for existing automated reports).
*/
// @TODO this will add filter, however if we need to control feature by FF we need conditional logic
public const array TYPES = [
['id' => 'exec_summary', 'name' => 'Exec Summary'],
['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],
['id' => 'product_feedback', 'name' => 'Product Feedback'],
['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],
// ['id' => 'questions', 'name' => 'Questions'],
// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],
];
public const array ALL_TYPES = [
...self::TYPES,
['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],
];
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';
/**
* Frequencies for standard (non-Ask Jiminny) reports.
*/
public const array FREQUENCIES = [
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
/**
* Frequencies for Ask Jiminny reports.
*/
public const array ASK_JIMINNY_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
];
public const string MEDIA_TYPE_PDF = 'pdf';
public const string MEDIA_TYPE_PODCAST = 'podcast';
public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];
public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];
public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];
public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];
public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];
public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];
public const int SENT_REPORT_AT_HOURS = 5;
public const string PDF_KEY = 'pdf';
public const string AUDIO_KEY = 'audio';
private const array ALL_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
private const string S3_DIR = 'reports';
private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];
private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly GroupRepository $groupRepository,
private readonly UserRepository $userRepository,
private readonly StageRepository $stageRepository,
private readonly DealStagesService $dealStagesService,
private readonly RecipientsService $recipientsService,
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly Webhook $webhookService,
private readonly BusDispatcher $dispatcher,
private readonly ActivityTypeService $activityTypeService,
private readonly PlaybookCategoryRepository $playbookCategoryRepository,
private readonly AskAnythingPromptService $askAnythingPromptService,
private readonly SearchRepository $activitySearchRepository,
private readonly AskAnythingRepository $askAnythingRepository,
) {
}
public static function getTypes(): array
{
$types = self::TYPES;
return array_map(static function ($type) {
return $type['id'];
}, $types);
}
public static function getCallTypes(): array
{
return array_map(static function ($callType) {
return $callType['id'];
}, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);
}
public static function getFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::FREQUENCIES);
}
// front-facing structure
public function getReportEnabledFieldData(bool $value = false): array
{
return [
'id' => 'report_enabled',
'label' => '',
'inputType' => InputTypeEnum::TOGGLE,
'value' => $value,
];
}
// Organizations = Teams
public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array
{
$options = $this->getTeams(partner: $partner);
if ($shortVersion) {
return [
'id' => 'organization',
'label' => 'Organization',
'options' => $options,
];
}
return [
'id' => 'organization',
'label' => 'Organization',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $options,
'value' => $value,
'dependencies' => [
'teams',
'deal_stage_at_call',
'current_deal_stage',
'recipients',
ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,
],
'dependsOn' => [],
];
}
// Teams = Groups
public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array
{
if ($shortVersion) {
return [
'id' => 'teams',
'label' => 'Team',
'options' => $options,
];
}
return [
'id' => 'teams',
'label' => 'Team',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => false,
'placeholder' => 'Select',
'options' => $options,
'value' => $value, // value should be an array of objects {id, name}
'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],
'dependsOn' => [],
];
}
public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array
{
$types = [];
if ($team instanceof Team) {
if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
$types = self::TYPES;
}
if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {
$types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];
}
} else {
$types = self::TYPES;
}
if ($shortVersion) {
return [
'id' => 'report_type',
'label' => 'Report Type',
'options' => $types,
];
}
return [
'id' => 'report_type',
'label' => 'Report Type',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $types,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getFrequencyFieldData(?string $value = null): array
{
return [
'id' => 'frequency',
'label' => 'Frequency',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => self::FREQUENCIES,
'value' => $value,
'dependencies' => ['period'],
'dependsOn' => [],
];
}
public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array
{
return [
'id' => 'period',
'label' => 'Select one-off period',
'inputType' => InputTypeEnum::DATE_RANGE,
'required' => true,
'placeholder' => 'Select',
'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],
'queryParams' => [
'startDate' => 'start_date_period',
'endDate' => 'end_date_period',
],
'dependencies' => [],
'dependsOn' => ['frequency'],
];
}
public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array
{
return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);
}
public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);
}
public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);
}
public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'deal_value',
'label' => 'Deal Value',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_deal_value',
'max' => 'max_deal_value',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array
{
$value = [];
$conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;
$dialerOn && $value[] = self::CALL_TYPE_DIALER;
return [
'id' => 'call_type',
'label' => 'Call Type',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => [
self::CALL_TYPE_CONFERENCE,
self::CALL_TYPE_DIALER,
],
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getMediaTypeFieldData(?AutomatedReport $report = null): array
{
$value = [];
if ($report) {
$value = $this->transformMediaTypes($report);
}
return [
'id' => 'media_types',
'label' => 'Export as',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => self::MEDIA_TYPE_OBJECTS,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'call_duration',
'label' => 'Call Duration',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_call_duration',
'max' => 'max_call_duration',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getRecipientsFieldData(?Team $team = null, array $value = []): array
{
return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);
}
public function getJiminnyRecipientsFieldData(array $value = []): array
{
return $this->recipientsService->getJiminnyRecipientsFieldData($value);
}
public function getAdditionalPromptInputFieldData(?string $value = null): array
{
return [
'id' => 'additional_prompt_input',
'label' => 'Special requirements',
'inputType' => InputTypeEnum::TEXTAREA,
'required' => false,
'placeholder' => 'What should be the focus of the report?',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCustomReportNameFieldData(?string $value = null): array
{
return [
'id' => 'custom_name',
'label' => 'Custom report name',
'inputType' => InputTypeEnum::TEXT,
'required' => false,
'placeholder' => 'Enter custom name',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
// data providers
public function getTeams(?Partner $partner = null): array
{
$teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);
$teamData = [];
foreach ($teams as $team) {
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
continue;
}
$teamData[] = $this->transformTeam($team);
}
return $teamData;
}
public function getTeamGroups(string $teamUuid): array
{
$data = [];
$team = $this->getTeam($teamUuid);
if ($team !== null) {
$groups = $team->groups()->get();
foreach ($groups as $group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
];
}
}
return $data;
}
public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array
{
$data = [];
$teams = $this->getTeams(partner: $partner);
foreach ($teams as $team) {
if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {
continue;
}
$data[] = [
'label' => $team['name'],
'groups' => $this->getTeamGroups($team['id']),
];
}
return $data;
}
public function getTeam(string $teamUuid): ?Team
{
return $this->teamRepository->idOrUuid($teamUuid);
}
public function getTeamById(int $teamId): ?Team
{
return $this->teamRepository->find($teamId);
}
public function getGroupsUuids(AutomatedReport $report): array
{
$uuids = [];
$reportGroups = $report->getGroups();
foreach ($reportGroups as $groupId) {
if ($group = $this->groupRepository->find($groupId)) {
$uuids[] = $group->getUuid();
}
}
return $uuids;
}
public function getPlaybookCategoriesUuids(AutomatedReport $report): array
{
$uuids = [];
$playbookCategories = $report->getPlaybookCategories();
foreach ($playbookCategories as $id) {
if ($category = $this->playbookCategoryRepository->find($id)) {
$uuids[] = $category->getUuid();
}
}
return $uuids;
}
public function getDealAtCallStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getDealAtCallStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getCurrentDealStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getCurrentDealStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getRecipients());
}
public function getJiminnyUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function extractUserUuids(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => $user->getUuid())
->values()
->all();
}
// get mail data
public function getRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getRecipients());
}
/**
* @return array<UserContract>
*/
public function getRecipientUserObjects(AutomatedReport $report): array
{
$userIds = $report->getRecipients()['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->values()
->all();
}
private function getJiminnyRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function buildRecipientUsers(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => [
'email' => $user->getEmailAddress(),
'name' => $user->getName(),
'timezone' => $user->getTimezone()->getName(),
])
->values()
->all();
}
public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array
{
if ($report->isAskJiminnyReport()) {
$recipients = $this->resolveAskJiminnyRecipients($report);
} else {
$recipients = $this->getRecipientUsers($report);
if ($includeJiminny) {
$recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));
}
}
$emails = [];
return array_values(array_filter(
$recipients,
static function ($recipient) use (&$emails) {
if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {
return false;
}
$emails[] = $recipient['email'];
return true;
}
));
}
private function resolveAskJiminnyRecipients(AutomatedReport $report): array
{
$recipients = [];
$creator = $report->getCreator();
if ($creator !== null) {
$recipients[] = [
'email' => $creator->getEmailAddress(),
'name' => $creator->getName(),
'timezone' => $creator->getTimezone()->getName(),
];
}
return array_merge(
$recipients,
$this->buildRecipientUsers($report->getRecipients()),
$this->getGroupRecipientUsers($report),
);
}
private function getGroupRecipientUsers(AutomatedReport $report): array
{
$users = [];
foreach ($report->getGroups() as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group === null) {
continue;
}
foreach ($group->getMembers() as $member) {
$users[] = [
'email' => $member->getEmailAddress(),
'name' => $member->getName(),
'timezone' => $member->getTimezone()->getName(),
];
}
}
return $users;
}
public function getReportTypeName(AutomatedReportResult $report): string
{
$type = $report->getReport()->getType();
$getType = $this->transformReportType($type);
return $getType['name'];
}
public function getReportPeriodName(AutomatedReportResult $report): string
{
$from = $report->getFromDate();
$to = $report->getToDate();
$frequency = $report->getReport()->getFrequency();
if ($from === null || $to === null) {
if (! $report->getReport()->isAskJiminnyReport()) {
$invalidPeriod = $from === null ? 'from' : 'to';
throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);
}
$timezone = $report->getReport()->getCreator()?->getTimezone();
$period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);
$from = $period['fromDate'];
$to = $period['toDate'];
}
return $this->formatReportPeriodName($frequency, $from, $to);
}
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 getReportTeamsName(AutomatedReportResult $report): string
{
$groups = $report->getGroups();
if (empty($groups)) {
return 'All';
}
// Get group names from repository
$groupNames = [];
foreach ($groups as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group) {
$groupNames[] = $group->getName();
}
}
if (count($groupNames) === 1) {
// Single team format
$teamsName = $groupNames[0];
} else {
// Multiple teams format
$teamsName = implode(', ', $groupNames);
}
return $teamsName;
}
public function getReportFileName(AutomatedReportResult $report): string
{
$customName = $report->getReport()->getCustomName();
$periodName = $this->getReportPeriodName($report);
$filenameSuffix = $this->getFilenameSuffix($report);
if ($customName) {
if ($filenameSuffix) {
$customName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$customName} - {$periodName}");
}
$baseName = $this->getReportTypeName($report);
if ($filenameSuffix) {
$baseName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}");
}
public function getReportFileNameWithExtension(AutomatedReportResult $result): string
{
$extension = $this->getMediaTypeMetadata($result)['extension'];
return $this->getReportFileName($result) . '.' . $extension;
}
public function sanitizeFileName(string $fileName): string
{
return str_replace(['/', '\\'], '-', $fileName);
}
public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool
{
$recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);
if (in_array($user->getId(), $recipientIds, true)) {
return true;
}
if ($report->isAskJiminnyReport()) {
$groupId = $user->getGroupId();
if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {
return true;
}
}
return false;
}
public function transformReportResults(Collection $automatedReportResults): array
{
$data = [];
foreach ($automatedReportResults as $automatedReportResult) {
/** @var AutomatedReportResult $automatedReportResult */
$report = $automatedReportResult->getReport();
$createdBy = $report->getCreator();
$creator = [
'id' => $createdBy?->getUuid(),
'name' => $createdBy?->getName(),
'email' => $createdBy?->getEmailAddress(),
'photoUrl' => $createdBy?->getPhotoUrl(),
];
$data[] = [
'id' => $automatedReportResult->getUuid(),
'name' => $automatedReportResult->getName(),
'frequency' => $this->transformFrequency($report->getFrequency()),
'recipients' => $this->buildRecipients($report),
'report_type' => $this->transformReportType($report->getType()),
'media_type' => $automatedReportResult->getMediaType(),
'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),
'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),
'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),
'creator' => $creator,
];
}
return $data;
}
private function buildRecipients(AutomatedReport $report): array
{
$creatorUuid = $report->getCreator()?->getUuid();
$recipients = array_values(array_filter(
$this->transformRecipients($report->getRecipients()),
static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,
));
if (! $report->isAskJiminnyReport()) {
return $recipients;
}
return [
...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),
...$recipients,
];
}
public function hasCallTypeConference(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);
}
public function hasCallTypeDialer(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);
}
// transformers
private function transformTeam(Team $team): array
{
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
return [];
}
return [
'id' => $team->getUuid(),
'name' => $team->getName(),
];
}
private function transformReportFullView(AutomatedReport $report): array
{
$base = $this->transformReportBase($report);
return $report->getType() === self::TYPE_ASK_JIMINNY
? $base + $this->transformAskJiminnyFields($report)
: $base + $this->transformStandardReportFields($report);
}
private function transformReportBase(AutomatedReport $report): array
{
return [
'id' => $report->getUuid(),
'organization' => $this->transformOrganization(team: $report->getTeam()),
'report_type' => $this->transformReportType($report->getType()),
'frequency' => $this->transformFrequency($report->getFrequency()),
];
}
private function transformStandardReportFields(AutomatedReport $report): array
{
$team = $report->getTeam();
return [
'report_enabled' => $report->getStatus(),
'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),
'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),
'deal_value_min' => $report->getDealValueMin(),
'deal_value_max' => $report->getDealValueMax(),
'call_types' => $this->transformCallType($report->getCallTypes()),
'media_types' => $this->transformMediaTypes($report),
'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),
'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),
'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),
'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),
'recipients' => $this->transformRecipients($report->getRecipients()),
'created_by' => $this->transformCreator($report->getCreator()),
'additional_prompt_input' => $report->getAdditionalPromptInput(),
'custom_name' => $report->getCustomName(),
'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),
'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),
'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),
];
}
private function transformAskJiminnyFields(AutomatedReport $report): array
{
$team = $report->getTeam();
$creatorId = $report->getAttribute('created_by');
$explicitUserIds = array_values(array_filter(
$report->getRecipients()['users'] ?? [],
static fn ($id) => $id !== $creatorId
));
return [
'report_name' => $report->getCustomName(),
'enabled' => $report->getStatus(),
'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),
'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),
'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),
'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),
];
}
private function transformOrganization(?Team $team): array
{
return [
'id' => $team?->getUuid(),
'name' => $team?->getName(),
];
}
private function transformReportType(string $type): array
{
foreach (self::ALL_TYPES as $typeItem) {
if ($typeItem['id'] === $type) {
return $typeItem;
}
}
return [];
}
private function transformCallType(array $types): array
{
$result = [];
$callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];
foreach ($types as $type) {
foreach ($callTypes as $callTypeItem) {
if ($callTypeItem['id'] === $type) {
$result[] = $callTypeItem;
break;
}
}
}
return $result;
}
private function transformMediaTypes(AutomatedReport $report): array
{
$values = [];
foreach ($report->getMediaTypes() as $mediaType) {
if (! in_array($mediaType, self::MEDIA_TYPES, true)) {
continue;
}
$values[] = match ($mediaType) {
self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,
self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,
};
}
return $values;
}
private function transformFrequency(string $frequency): array
{
foreach (self::ALL_FREQUENCIES as $frequencyItem) {
if ($frequencyItem['id'] === $frequency) {
return $frequencyItem;
}
}
return [];
}
public function transformDurationToMinutes(?int $duration): ?int
{
if (! $duration) {
return null;
}
return (int) ($duration / 60);
}
private function transformGroups(?Team $team, array $groupsIds): array
{
if (empty($groupsIds) || ! $team) {
return [];
}
$data = [];
foreach ($groupsIds as $groupId) {
$group = $team->groups()->where('id', $groupId)->first();
if ($group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
'photoUrl' => $group->getPhotoUrl(),
];
}
}
return $data;
}
private function transformStages(?Team $team, array $stagesIds): array
{
if (empty($stagesIds) || ! $team) {
return [];
}
$data = [];
foreach ($stagesIds as $stageId) {
$stage = $team->stages()->where('id', $stageId)->first();
if ($stage) {
$data[] = [
'id' => $stage->getUuid(),
'name' => $stage->getName(),
];
}
}
return $data;
}
private function transformRecipients(array $recipients): array
{
$users = [];
foreach ($recipients['users'] ?? [] as $userId) {
$users[] = $this->transformUser($userId);
}
return $users;
}
private function transformCreator(?User $user): ?array
{
if ($user === null) {
return null;
}
return $this->transformUser($user->getId());
}
private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array
{
if ($prompt === null) {
return null;
}
return [
'id' => $prompt->getUuid(),
'name' => $prompt->getTitle(),
];
}
private function transformSafeSearch(?Search $search): ?array
{
if ($search === null) {
return null;
}
return [
'id' => $search->getUuid(),
'name' => $search->getName(),
];
}
private function transformUser(int $userId): array
{
/* @var ?User $user */
$user = $this->userRepository->find($userId);
return [
'id' => $user?->getUuid(),
'name' => $user?->getName(),
'email' => $user?->getEmailAddress(),
'photoUrl' => $user?->getPhotoUrl(),
];
}
public function create(array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$validatedData['created_by'] = auth()->id();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
public function update(string $uuid, array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$report = $this->automatedReportsRepository->findByUuid($uuid);
if (! $report) {
throw new InvalidArgumentException('Report not found');
}
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
/**
* Create an Ask Jiminny report.
*/
public function createAskJiminnyReport(array $data, User $creator): array
{
$validatedData = $this->validateAskJiminnyReportData($data, $creator);
$validatedData['created_by'] = $creator->getId();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
return $this->transformReportFullView($automatedReport);
}
/**
* Update an Ask Jiminny report.
*/
public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array
{
if (! $report->isAskJiminnyReport()) {
throw new InvalidArgumentException('Report is not an Ask Jiminny report');
}
$validatedData = $this->validateAskJiminnyReportData($data, $user);
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
return $this->transformReportFullView($automatedReport);
}
public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array
{
if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {
throw new InvalidArgumentException(
'This report is missing a saved search or prompt. ' .
'Edit the report to complete the setup before enabling it.'
);
}
$this->automatedReportsRepository->update($report, ['status' => $status]);
return $this->transformReportFullView($report->fresh());
}
/**
* Validate and transform data for Ask Jiminny reports.
*/
private function validateAskJiminnyReportData(array $data, User $user): array
{
// Validate name
$name = trim($data['report_name'] ?? '');
if (empty($name)) {
throw new InvalidArgumentException('Report name is required');
}
if (mb_strlen($name) > 50) {
throw new InvalidArgumentException('Report name must be 50 characters or less');
}
// Validate frequency (only daily, weekly, monthly for Ask Jiminny)
$frequency = $data['frequency'] ?? null;
$askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];
if (! in_array($frequency, $askJiminnyFrequencies, true)) {
throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');
}
// Validate expiration date
$expiresAt = $data['expires_on'] ?? null;
if (empty($expiresAt)) {
throw new InvalidArgumentException('Expiration date is required');
}
try {
$expiresAtDate = Carbon::parse($expiresAt);
} catch (InvalidFormatException $e) {
throw new InvalidArgumentException('Expiration date format is invalid');
}
$maxExpiration = Carbon::now()->addYear()->endOfDay();
if ($expiresAtDate->gt($maxExpiration)) {
throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');
}
if ($expiresAtDate->isPast()) {
throw new InvalidArgumentException('Expiration date cannot be in the past');
}
// Validate saved search
$activitySearchId = $data['saved_search'] ?? null;
if (empty($activitySearchId)) {
throw new InvalidArgumentException('Saved search is required');
}
$savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);
if (! $savedSearch) {
throw new InvalidArgumentException('Saved search not found or does not belong to you');
}
// Validate saved prompt
$askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;
if (empty($askAnythingPromptId)) {
throw new InvalidArgumentException('Ask Jiminny prompt is required');
}
$prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);
if (! $prompt) {
throw new InvalidArgumentException('Ask Jiminny prompt not found');
}
// Validate status
$status = $data['enabled'] ?? false;
$recipientUserIds = [$user->getId()];
if (! empty($data['share_users'])) {
$sharedUserIds = $this->validateAndGetUserIdsByTeam(
$user->team,
(array) $data['share_users']
);
$recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);
}
$sharedGroupIds = [];
if (! empty($data['share_teams'])) {
$sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);
}
$recipientUserIds = array_values(array_unique($recipientUserIds));
return [
'team_id' => $user->getTeamId(),
'type' => self::TYPE_ASK_JIMINNY,
'status' => (bool) $status,
'frequency' => $frequency,
'custom_name' => $name,
'activity_search_id' => $savedSearch->getId(),
'ask_anything_prompt_id' => $prompt->getId(),
'expires_at' => $expiresAtDate->toDateString(),
'media_types' => [self::MEDIA_TYPE_PDF],
'call_types' => [],
'recipients' => ['users' => $recipientUserIds],
'groups' => $sharedGroupIds,
];
}
public static function getAskJiminnyFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::ASK_JIMINNY_FREQUENCIES);
}
public function getAskJiminnyReportFilters(User $user): array
{
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
return [
[
'id' => 'prompt',
'label' => 'Prompt',
'options' => $prompts,
],
[
'id' => 'saved_search',
'label' => 'Saved Search',
'options' => $savedSearches,
],
];
}
public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array
{
$team = $user->getTeam();
$userTimezone = $user->getTimezone();
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
$teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [
'id' => $group->getUuid(),
'name' => $group->getName(),
])->values()->all();
$shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];
$sharedTeamsValue = [];
$sharedUsersValue = [];
if ($report) {
$sharedTeamsValue = $this->transformGroups($team, $report->getGroups());
$recipientUserIds = $report->getRecipients()['users'] ?? [];
$creatorId = $report->getAttribute('created_by');
$sharedUserIds = array_values(array_filter(
$recipientUserIds,
static fn ($id) => $id !== $creatorId
));
$sharedUsersValue = collect($sharedUserIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (User $u) => [
'id' => $u->getUuid(),
'name' => $u->getName(),
])
->values()
->all();
}
return [
'fields' => [
[
'id' => 'enabled',
'inputType' => InputTypeEnum::TOGGLE,
'label' => '',
'value' => $report?->getStatus() ?? false,
],
[
'id' => 'report_name',
'inputType' => InputTypeEnum::TEXT,
'label' => 'Name',
'placeholder' => 'Enter name',
'required' => true,
'validation' => ['maxLength' => 50],
'value' => $report?->getCustomName() ?? '',
...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58034
|
2042
|
9
|
2026-05-19T11:29:32.601659+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190172601_m1.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskAnythingPromptServiceTest
Run 'AskAnythingPromptServiceTest'...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects<br/>Some incoming commits are not fetched<br/>","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,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskAnythingPromptServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskAnythingPromptServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4959269709546757893
|
-8637481863596221053
|
idle
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskAnythingPromptServiceTest
Run 'AskAnythingPromptServiceTest'
SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV (docker)APP₴82APP (-zslreturn new JsonResponse(['error'$error],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLaterMore>0.Support Daily - in 31 m100% <78• Tue 19 May 14:29:32ED→QDescribe what you are looking forJiminny... vNikolay Yankov6 0scnicre# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...• Messages+. Direct messages8. Nikolay Yankov% Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomove Todor Stamatov "Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...Add canvas@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNewNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098l:: AppsS Jira CloudToast1Message Nikolay Yankov+...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58035
|
2043
|
10
|
2026-05-19T11:29:44.922816+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190184922_m2.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskAnythingPromptServiceTest
Run 'AskAnythingPromptServiceTest'
Debug 'AskAnythingPromptServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
100
3
34
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Kiosk\AutomatedReports;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Carbon\Exceptions\InvalidFormatException;
use DateTime;
use DateTimeInterface;
use DateTimeZone;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\ActivitySearch\FilterDefinition\InputTypeEnum;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Partner;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Throwable;
class AutomatedReportsService
{
public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';
public const string TYPE_ASK_JIMINNY = 'ask_jiminny';
/**
* Standard report types (used by kiosk for existing automated reports).
*/
// @TODO this will add filter, however if we need to control feature by FF we need conditional logic
public const array TYPES = [
['id' => 'exec_summary', 'name' => 'Exec Summary'],
['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],
['id' => 'product_feedback', 'name' => 'Product Feedback'],
['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],
// ['id' => 'questions', 'name' => 'Questions'],
// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],
];
public const array ALL_TYPES = [
...self::TYPES,
['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],
];
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';
/**
* Frequencies for standard (non-Ask Jiminny) reports.
*/
public const array FREQUENCIES = [
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
/**
* Frequencies for Ask Jiminny reports.
*/
public const array ASK_JIMINNY_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
];
public const string MEDIA_TYPE_PDF = 'pdf';
public const string MEDIA_TYPE_PODCAST = 'podcast';
public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];
public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];
public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];
public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];
public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];
public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];
public const int SENT_REPORT_AT_HOURS = 5;
public const string PDF_KEY = 'pdf';
public const string AUDIO_KEY = 'audio';
private const array ALL_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
private const string S3_DIR = 'reports';
private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];
private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly GroupRepository $groupRepository,
private readonly UserRepository $userRepository,
private readonly StageRepository $stageRepository,
private readonly DealStagesService $dealStagesService,
private readonly RecipientsService $recipientsService,
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly Webhook $webhookService,
private readonly BusDispatcher $dispatcher,
private readonly ActivityTypeService $activityTypeService,
private readonly PlaybookCategoryRepository $playbookCategoryRepository,
private readonly AskAnythingPromptService $askAnythingPromptService,
private readonly SearchRepository $activitySearchRepository,
private readonly AskAnythingRepository $askAnythingRepository,
) {
}
public static function getTypes(): array
{
$types = self::TYPES;
return array_map(static function ($type) {
return $type['id'];
}, $types);
}
public static function getCallTypes(): array
{
return array_map(static function ($callType) {
return $callType['id'];
}, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);
}
public static function getFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::FREQUENCIES);
}
// front-facing structure
public function getReportEnabledFieldData(bool $value = false): array
{
return [
'id' => 'report_enabled',
'label' => '',
'inputType' => InputTypeEnum::TOGGLE,
'value' => $value,
];
}
// Organizations = Teams
public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array
{
$options = $this->getTeams(partner: $partner);
if ($shortVersion) {
return [
'id' => 'organization',
'label' => 'Organization',
'options' => $options,
];
}
return [
'id' => 'organization',
'label' => 'Organization',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $options,
'value' => $value,
'dependencies' => [
'teams',
'deal_stage_at_call',
'current_deal_stage',
'recipients',
ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,
],
'dependsOn' => [],
];
}
// Teams = Groups
public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array
{
if ($shortVersion) {
return [
'id' => 'teams',
'label' => 'Team',
'options' => $options,
];
}
return [
'id' => 'teams',
'label' => 'Team',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => false,
'placeholder' => 'Select',
'options' => $options,
'value' => $value, // value should be an array of objects {id, name}
'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],
'dependsOn' => [],
];
}
public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array
{
$types = [];
if ($team instanceof Team) {
if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
$types = self::TYPES;
}
if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {
$types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];
}
} else {
$types = self::TYPES;
}
if ($shortVersion) {
return [
'id' => 'report_type',
'label' => 'Report Type',
'options' => $types,
];
}
return [
'id' => 'report_type',
'label' => 'Report Type',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $types,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getFrequencyFieldData(?string $value = null): array
{
return [
'id' => 'frequency',
'label' => 'Frequency',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => self::FREQUENCIES,
'value' => $value,
'dependencies' => ['period'],
'dependsOn' => [],
];
}
public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array
{
return [
'id' => 'period',
'label' => 'Select one-off period',
'inputType' => InputTypeEnum::DATE_RANGE,
'required' => true,
'placeholder' => 'Select',
'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],
'queryParams' => [
'startDate' => 'start_date_period',
'endDate' => 'end_date_period',
],
'dependencies' => [],
'dependsOn' => ['frequency'],
];
}
public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array
{
return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);
}
public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);
}
public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);
}
public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'deal_value',
'label' => 'Deal Value',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_deal_value',
'max' => 'max_deal_value',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array
{
$value = [];
$conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;
$dialerOn && $value[] = self::CALL_TYPE_DIALER;
return [
'id' => 'call_type',
'label' => 'Call Type',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => [
self::CALL_TYPE_CONFERENCE,
self::CALL_TYPE_DIALER,
],
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getMediaTypeFieldData(?AutomatedReport $report = null): array
{
$value = [];
if ($report) {
$value = $this->transformMediaTypes($report);
}
return [
'id' => 'media_types',
'label' => 'Export as',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => self::MEDIA_TYPE_OBJECTS,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'call_duration',
'label' => 'Call Duration',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_call_duration',
'max' => 'max_call_duration',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getRecipientsFieldData(?Team $team = null, array $value = []): array
{
return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);
}
public function getJiminnyRecipientsFieldData(array $value = []): array
{
return $this->recipientsService->getJiminnyRecipientsFieldData($value);
}
public function getAdditionalPromptInputFieldData(?string $value = null): array
{
return [
'id' => 'additional_prompt_input',
'label' => 'Special requirements',
'inputType' => InputTypeEnum::TEXTAREA,
'required' => false,
'placeholder' => 'What should be the focus of the report?',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCustomReportNameFieldData(?string $value = null): array
{
return [
'id' => 'custom_name',
'label' => 'Custom report name',
'inputType' => InputTypeEnum::TEXT,
'required' => false,
'placeholder' => 'Enter custom name',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
// data providers
public function getTeams(?Partner $partner = null): array
{
$teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);
$teamData = [];
foreach ($teams as $team) {
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
continue;
}
$teamData[] = $this->transformTeam($team);
}
return $teamData;
}
public function getTeamGroups(string $teamUuid): array
{
$data = [];
$team = $this->getTeam($teamUuid);
if ($team !== null) {
$groups = $team->groups()->get();
foreach ($groups as $group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
];
}
}
return $data;
}
public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array
{
$data = [];
$teams = $this->getTeams(partner: $partner);
foreach ($teams as $team) {
if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {
continue;
}
$data[] = [
'label' => $team['name'],
'groups' => $this->getTeamGroups($team['id']),
];
}
return $data;
}
public function getTeam(string $teamUuid): ?Team
{
return $this->teamRepository->idOrUuid($teamUuid);
}
public function getTeamById(int $teamId): ?Team
{
return $this->teamRepository->find($teamId);
}
public function getGroupsUuids(AutomatedReport $report): array
{
$uuids = [];
$reportGroups = $report->getGroups();
foreach ($reportGroups as $groupId) {
if ($group = $this->groupRepository->find($groupId)) {
$uuids[] = $group->getUuid();
}
}
return $uuids;
}
public function getPlaybookCategoriesUuids(AutomatedReport $report): array
{
$uuids = [];
$playbookCategories = $report->getPlaybookCategories();
foreach ($playbookCategories as $id) {
if ($category = $this->playbookCategoryRepository->find($id)) {
$uuids[] = $category->getUuid();
}
}
return $uuids;
}
public function getDealAtCallStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getDealAtCallStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getCurrentDealStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getCurrentDealStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getRecipients());
}
public function getJiminnyUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function extractUserUuids(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => $user->getUuid())
->values()
->all();
}
// get mail data
public function getRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getRecipients());
}
/**
* @return array<UserContract>
*/
public function getRecipientUserObjects(AutomatedReport $report): array
{
$userIds = $report->getRecipients()['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->values()
->all();
}
private function getJiminnyRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function buildRecipientUsers(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => [
'email' => $user->getEmailAddress(),
'name' => $user->getName(),
'timezone' => $user->getTimezone()->getName(),
])
->values()
->all();
}
public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array
{
if ($report->isAskJiminnyReport()) {
$recipients = $this->resolveAskJiminnyRecipients($report);
} else {
$recipients = $this->getRecipientUsers($report);
if ($includeJiminny) {
$recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));
}
}
$emails = [];
return array_values(array_filter(
$recipients,
static function ($recipient) use (&$emails) {
if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {
return false;
}
$emails[] = $recipient['email'];
return true;
}
));
}
private function resolveAskJiminnyRecipients(AutomatedReport $report): array
{
$recipients = [];
$creator = $report->getCreator();
if ($creator !== null) {
$recipients[] = [
'email' => $creator->getEmailAddress(),
'name' => $creator->getName(),
'timezone' => $creator->getTimezone()->getName(),
];
}
return array_merge(
$recipients,
$this->buildRecipientUsers($report->getRecipients()),
$this->getGroupRecipientUsers($report),
);
}
private function getGroupRecipientUsers(AutomatedReport $report): array
{
$users = [];
foreach ($report->getGroups() as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group === null) {
continue;
}
foreach ($group->getMembers() as $member) {
$users[] = [
'email' => $member->getEmailAddress(),
'name' => $member->getName(),
'timezone' => $member->getTimezone()->getName(),
];
}
}
return $users;
}
public function getReportTypeName(AutomatedReportResult $report): string
{
$type = $report->getReport()->getType();
$getType = $this->transformReportType($type);
return $getType['name'];
}
public function getReportPeriodName(AutomatedReportResult $report): string
{
$from = $report->getFromDate();
$to = $report->getToDate();
$frequency = $report->getReport()->getFrequency();
if ($from === null || $to === null) {
if (! $report->getReport()->isAskJiminnyReport()) {
$invalidPeriod = $from === null ? 'from' : 'to';
throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);
}
$timezone = $report->getReport()->getCreator()?->getTimezone();
$period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);
$from = $period['fromDate'];
$to = $period['toDate'];
}
return $this->formatReportPeriodName($frequency, $from, $to);
}
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 getReportTeamsName(AutomatedReportResult $report): string
{
$groups = $report->getGroups();
if (empty($groups)) {
return 'All';
}
// Get group names from repository
$groupNames = [];
foreach ($groups as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group) {
$groupNames[] = $group->getName();
}
}
if (count($groupNames) === 1) {
// Single team format
$teamsName = $groupNames[0];
} else {
// Multiple teams format
$teamsName = implode(', ', $groupNames);
}
return $teamsName;
}
public function getReportFileName(AutomatedReportResult $report): string
{
$customName = $report->getReport()->getCustomName();
$periodName = $this->getReportPeriodName($report);
$filenameSuffix = $this->getFilenameSuffix($report);
if ($customName) {
if ($filenameSuffix) {
$customName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$customName} - {$periodName}");
}
$baseName = $this->getReportTypeName($report);
if ($filenameSuffix) {
$baseName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}");
}
public function getReportFileNameWithExtension(AutomatedReportResult $result): string
{
$extension = $this->getMediaTypeMetadata($result)['extension'];
return $this->getReportFileName($result) . '.' . $extension;
}
public function sanitizeFileName(string $fileName): string
{
return str_replace(['/', '\\'], '-', $fileName);
}
public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool
{
$recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);
if (in_array($user->getId(), $recipientIds, true)) {
return true;
}
if ($report->isAskJiminnyReport()) {
$groupId = $user->getGroupId();
if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {
return true;
}
}
return false;
}
public function transformReportResults(Collection $automatedReportResults): array
{
$data = [];
foreach ($automatedReportResults as $automatedReportResult) {
/** @var AutomatedReportResult $automatedReportResult */
$report = $automatedReportResult->getReport();
$createdBy = $report->getCreator();
$creator = [
'id' => $createdBy?->getUuid(),
'name' => $createdBy?->getName(),
'email' => $createdBy?->getEmailAddress(),
'photoUrl' => $createdBy?->getPhotoUrl(),
];
$data[] = [
'id' => $automatedReportResult->getUuid(),
'name' => $automatedReportResult->getName(),
'frequency' => $this->transformFrequency($report->getFrequency()),
'recipients' => $this->buildRecipients($report),
'report_type' => $this->transformReportType($report->getType()),
'media_type' => $automatedReportResult->getMediaType(),
'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),
'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),
'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),
'creator' => $creator,
];
}
return $data;
}
private function buildRecipients(AutomatedReport $report): array
{
$creatorUuid = $report->getCreator()?->getUuid();
$recipients = array_values(array_filter(
$this->transformRecipients($report->getRecipients()),
static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,
));
if (! $report->isAskJiminnyReport()) {
return $recipients;
}
return [
...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),
...$recipients,
];
}
public function hasCallTypeConference(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);
}
public function hasCallTypeDialer(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);
}
// transformers
private function transformTeam(Team $team): array
{
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
return [];
}
return [
'id' => $team->getUuid(),
'name' => $team->getName(),
];
}
private function transformReportFullView(AutomatedReport $report): array
{
$base = $this->transformReportBase($report);
return $report->getType() === self::TYPE_ASK_JIMINNY
? $base + $this->transformAskJiminnyFields($report)
: $base + $this->transformStandardReportFields($report);
}
private function transformReportBase(AutomatedReport $report): array
{
return [
'id' => $report->getUuid(),
'organization' => $this->transformOrganization(team: $report->getTeam()),
'report_type' => $this->transformReportType($report->getType()),
'frequency' => $this->transformFrequency($report->getFrequency()),
];
}
private function transformStandardReportFields(AutomatedReport $report): array
{
$team = $report->getTeam();
return [
'report_enabled' => $report->getStatus(),
'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),
'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),
'deal_value_min' => $report->getDealValueMin(),
'deal_value_max' => $report->getDealValueMax(),
'call_types' => $this->transformCallType($report->getCallTypes()),
'media_types' => $this->transformMediaTypes($report),
'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),
'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),
'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),
'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),
'recipients' => $this->transformRecipients($report->getRecipients()),
'created_by' => $this->transformCreator($report->getCreator()),
'additional_prompt_input' => $report->getAdditionalPromptInput(),
'custom_name' => $report->getCustomName(),
'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),
'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),
'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),
];
}
private function transformAskJiminnyFields(AutomatedReport $report): array
{
$team = $report->getTeam();
$creatorId = $report->getAttribute('created_by');
$explicitUserIds = array_values(array_filter(
$report->getRecipients()['users'] ?? [],
static fn ($id) => $id !== $creatorId
));
return [
'report_name' => $report->getCustomName(),
'enabled' => $report->getStatus(),
'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),
'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),
'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),
'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),
];
}
private function transformOrganization(?Team $team): array
{
return [
'id' => $team?->getUuid(),
'name' => $team?->getName(),
];
}
private function transformReportType(string $type): array
{
foreach (self::ALL_TYPES as $typeItem) {
if ($typeItem['id'] === $type) {
return $typeItem;
}
}
return [];
}
private function transformCallType(array $types): array
{
$result = [];
$callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];
foreach ($types as $type) {
foreach ($callTypes as $callTypeItem) {
if ($callTypeItem['id'] === $type) {
$result[] = $callTypeItem;
break;
}
}
}
return $result;
}
private function transformMediaTypes(AutomatedReport $report): array
{
$values = [];
foreach ($report->getMediaTypes() as $mediaType) {
if (! in_array($mediaType, self::MEDIA_TYPES, true)) {
continue;
}
$values[] = match ($mediaType) {
self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,
self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,
};
}
return $values;
}
private function transformFrequency(string $frequency): array
{
foreach (self::ALL_FREQUENCIES as $frequencyItem) {
if ($frequencyItem['id'] === $frequency) {
return $frequencyItem;
}
}
return [];
}
public function transformDurationToMinutes(?int $duration): ?int
{
if (! $duration) {
return null;
}
return (int) ($duration / 60);
}
private function transformGroups(?Team $team, array $groupsIds): array
{
if (empty($groupsIds) || ! $team) {
return [];
}
$data = [];
foreach ($groupsIds as $groupId) {
$group = $team->groups()->where('id', $groupId)->first();
if ($group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
'photoUrl' => $group->getPhotoUrl(),
];
}
}
return $data;
}
private function transformStages(?Team $team, array $stagesIds): array
{
if (empty($stagesIds) || ! $team) {
return [];
}
$data = [];
foreach ($stagesIds as $stageId) {
$stage = $team->stages()->where('id', $stageId)->first();
if ($stage) {
$data[] = [
'id' => $stage->getUuid(),
'name' => $stage->getName(),
];
}
}
return $data;
}
private function transformRecipients(array $recipients): array
{
$users = [];
foreach ($recipients['users'] ?? [] as $userId) {
$users[] = $this->transformUser($userId);
}
return $users;
}
private function transformCreator(?User $user): ?array
{
if ($user === null) {
return null;
}
return $this->transformUser($user->getId());
}
private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array
{
if ($prompt === null) {
return null;
}
return [
'id' => $prompt->getUuid(),
'name' => $prompt->getTitle(),
];
}
private function transformSafeSearch(?Search $search): ?array
{
if ($search === null) {
return null;
}
return [
'id' => $search->getUuid(),
'name' => $search->getName(),
];
}
private function transformUser(int $userId): array
{
/* @var ?User $user */
$user = $this->userRepository->find($userId);
return [
'id' => $user?->getUuid(),
'name' => $user?->getName(),
'email' => $user?->getEmailAddress(),
'photoUrl' => $user?->getPhotoUrl(),
];
}
public function create(array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$validatedData['created_by'] = auth()->id();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
public function update(string $uuid, array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$report = $this->automatedReportsRepository->findByUuid($uuid);
if (! $report) {
throw new InvalidArgumentException('Report not found');
}
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
/**
* Create an Ask Jiminny report.
*/
public function createAskJiminnyReport(array $data, User $creator): array
{
$validatedData = $this->validateAskJiminnyReportData($data, $creator);
$validatedData['created_by'] = $creator->getId();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
return $this->transformReportFullView($automatedReport);
}
/**
* Update an Ask Jiminny report.
*/
public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array
{
if (! $report->isAskJiminnyReport()) {
throw new InvalidArgumentException('Report is not an Ask Jiminny report');
}
$validatedData = $this->validateAskJiminnyReportData($data, $user);
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
return $this->transformReportFullView($automatedReport);
}
public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array
{
if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {
throw new InvalidArgumentException(
'This report is missing a saved search or prompt. ' .
'Edit the report to complete the setup before enabling it.'
);
}
$this->automatedReportsRepository->update($report, ['status' => $status]);
return $this->transformReportFullView($report->fresh());
}
/**
* Validate and transform data for Ask Jiminny reports.
*/
private function validateAskJiminnyReportData(array $data, User $user): array
{
// Validate name
$name = trim($data['report_name'] ?? '');
if (empty($name)) {
throw new InvalidArgumentException('Report name is required');
}
if (mb_strlen($name) > 50) {
throw new InvalidArgumentException('Report name must be 50 characters or less');
}
// Validate frequency (only daily, weekly, monthly for Ask Jiminny)
$frequency = $data['frequency'] ?? null;
$askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];
if (! in_array($frequency, $askJiminnyFrequencies, true)) {
throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');
}
// Validate expiration date
$expiresAt = $data['expires_on'] ?? null;
if (empty($expiresAt)) {
throw new InvalidArgumentException('Expiration date is required');
}
try {
$expiresAtDate = Carbon::parse($expiresAt);
} catch (InvalidFormatException $e) {
throw new InvalidArgumentException('Expiration date format is invalid');
}
$maxExpiration = Carbon::now()->addYear()->endOfDay();
if ($expiresAtDate->gt($maxExpiration)) {
throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');
}
if ($expiresAtDate->isPast()) {
throw new InvalidArgumentException('Expiration date cannot be in the past');
}
// Validate saved search
$activitySearchId = $data['saved_search'] ?? null;
if (empty($activitySearchId)) {
throw new InvalidArgumentException('Saved search is required');
}
$savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);
if (! $savedSearch) {
throw new InvalidArgumentException('Saved search not found or does not belong to you');
}
// Validate saved prompt
$askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;
if (empty($askAnythingPromptId)) {
throw new InvalidArgumentException('Ask Jiminny prompt is required');
}
$prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);
if (! $prompt) {
throw new InvalidArgumentException('Ask Jiminny prompt not found');
}
// Validate status
$status = $data['enabled'] ?? false;
$recipientUserIds = [$user->getId()];
if (! empty($data['share_users'])) {
$sharedUserIds = $this->validateAndGetUserIdsByTeam(
$user->team,
(array) $data['share_users']
);
$recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);
}
$sharedGroupIds = [];
if (! empty($data['share_teams'])) {
$sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);
}
$recipientUserIds = array_values(array_unique($recipientUserIds));
return [
'team_id' => $user->getTeamId(),
'type' => self::TYPE_ASK_JIMINNY,
'status' => (bool) $status,
'frequency' => $frequency,
'custom_name' => $name,
'activity_search_id' => $savedSearch->getId(),
'ask_anything_prompt_id' => $prompt->getId(),
'expires_at' => $expiresAtDate->toDateString(),
'media_types' => [self::MEDIA_TYPE_PDF],
'call_types' => [],
'recipients' => ['users' => $recipientUserIds],
'groups' => $sharedGroupIds,
];
}
public static function getAskJiminnyFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::ASK_JIMINNY_FREQUENCIES);
}
public function getAskJiminnyReportFilters(User $user): array
{
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
return [
[
'id' => 'prompt',
'label' => 'Prompt',
'options' => $prompts,
],
[
'id' => 'saved_search',
'label' => 'Saved Search',
'options' => $savedSearches,
],
];
}
public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array
{
$team = $user->getTeam();
$userTimezone = $user->getTimezone();
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
$teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [
'id' => $group->getUuid(),
'name' => $group->getName(),
])->values()->all();
$shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];
$sharedTeamsValue = [];
$sharedUsersValue = [];
if ($report) {
$sharedTeamsValue = $this->transformGroups($team, $report->getGroups());
$recipientUserIds = $report->getRecipients()['users'] ?? [];
$creatorId = $report->getAttribute('created_by');
$sharedUserIds = array_values(array_filter(
$recipientUserIds,
static fn ($id) => $id !== $creatorId
));
$sharedUsersValue = collect($sharedUserIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (User $u) => [
'id' => $u->getUuid(),
'name' => $u->getName(),
])
->values()
->all();
}
return [
'fields' => [
[
'id' => 'enabled',
'inputType' => InputTypeEnum::TOGGLE,
'label' => '',
'value' => $report?->getStatus() ?? false,
],
[
'id' => 'report_name',
'inputType' => InputTypeEnum::TEXT,
'label' => 'Name',
'placeholder' => 'Enter name',
'required' => true,
'validation' => ['maxLength' => 50],
'value' => $report?->getCustomName() ?? '',
...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.10405585,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects<br/>Some incoming commits are not fetched<br/>","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.8194814,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskAnythingPromptServiceTest","depth":6,"bounds":{"left":0.83477396,"top":0.019952115,"width":0.080784574,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskAnythingPromptServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskAnythingPromptServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"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},"on_screen":true,"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},"on_screen":true,"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},"on_screen":true,"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},"on_screen":true,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"100","depth":4,"bounds":{"left":0.37599733,"top":0.15003991,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.3899601,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"34","depth":4,"bounds":{"left":0.39993352,"top":0.15003991,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.4192154,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"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\\Services\\Kiosk\\AutomatedReports;\n\nuse Carbon\\CarbonImmutable;\nuse Carbon\\CarbonInterface;\nuse Carbon\\Exceptions\\InvalidFormatException;\nuse DateTime;\nuse DateTimeInterface;\nuse DateTimeZone;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\InputTypeEnum;\nuse Jiminny\\Component\\AskAnything\\AskAnythingPromptService;\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Contracts\\Repositories\\PlaybookCategoryRepository;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\ModelNotFoundException;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Partner;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Repositories\\GroupRepository;\nuse Jiminny\\Repositories\\SearchRepository;\nuse Jiminny\\Repositories\\StageRepository;\nuse Throwable;\n\nclass AutomatedReportsService\n{\n public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';\n public const string TYPE_ASK_JIMINNY = 'ask_jiminny';\n\n /**\n * Standard report types (used by kiosk for existing automated reports).\n */\n // @TODO this will add filter, however if we need to control feature by FF we need conditional logic\n public const array TYPES = [\n ['id' => 'exec_summary', 'name' => 'Exec Summary'],\n ['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],\n ['id' => 'product_feedback', 'name' => 'Product Feedback'],\n ['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],\n// ['id' => 'questions', 'name' => 'Questions'],\n// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],\n ];\n\n public const array ALL_TYPES = [\n ...self::TYPES,\n ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],\n ];\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\n /**\n * Frequencies for standard (non-Ask Jiminny) reports.\n */\n public const array FREQUENCIES = [\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n\n /**\n * Frequencies for Ask Jiminny reports.\n */\n public const array ASK_JIMINNY_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ];\n\n public const string MEDIA_TYPE_PDF = 'pdf';\n public const string MEDIA_TYPE_PODCAST = 'podcast';\n public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];\n public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];\n public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];\n public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];\n\n public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];\n public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];\n public const int SENT_REPORT_AT_HOURS = 5;\n public const string PDF_KEY = 'pdf';\n public const string AUDIO_KEY = 'audio';\n\n private const array ALL_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n private const string S3_DIR = 'reports';\n private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];\n private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];\n\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly GroupRepository $groupRepository,\n private readonly UserRepository $userRepository,\n private readonly StageRepository $stageRepository,\n private readonly DealStagesService $dealStagesService,\n private readonly RecipientsService $recipientsService,\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly Webhook $webhookService,\n private readonly BusDispatcher $dispatcher,\n private readonly ActivityTypeService $activityTypeService,\n private readonly PlaybookCategoryRepository $playbookCategoryRepository,\n private readonly AskAnythingPromptService $askAnythingPromptService,\n private readonly SearchRepository $activitySearchRepository,\n private readonly AskAnythingRepository $askAnythingRepository,\n ) {\n }\n\n public static function getTypes(): array\n {\n $types = self::TYPES;\n\n return array_map(static function ($type) {\n return $type['id'];\n }, $types);\n }\n\n public static function getCallTypes(): array\n {\n return array_map(static function ($callType) {\n return $callType['id'];\n }, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);\n }\n\n public static function getFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::FREQUENCIES);\n }\n\n // front-facing structure\n public function getReportEnabledFieldData(bool $value = false): array\n {\n return [\n 'id' => 'report_enabled',\n 'label' => '',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'value' => $value,\n ];\n }\n\n // Organizations = Teams\n public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array\n {\n $options = $this->getTeams(partner: $partner);\n\n if ($shortVersion) {\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value,\n 'dependencies' => [\n 'teams',\n 'deal_stage_at_call',\n 'current_deal_stage',\n 'recipients',\n ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,\n ],\n 'dependsOn' => [],\n ];\n }\n\n // Teams = Groups\n public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array\n {\n if ($shortVersion) {\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value, // value should be an array of objects {id, name}\n 'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],\n 'dependsOn' => [],\n ];\n }\n\n public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array\n {\n $types = [];\n if ($team instanceof Team) {\n if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n $types = self::TYPES;\n }\n if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {\n $types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];\n }\n } else {\n $types = self::TYPES;\n }\n\n if ($shortVersion) {\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'options' => $types,\n ];\n }\n\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $types,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getFrequencyFieldData(?string $value = null): array\n {\n return [\n 'id' => 'frequency',\n 'label' => 'Frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::FREQUENCIES,\n 'value' => $value,\n 'dependencies' => ['period'],\n 'dependsOn' => [],\n ];\n }\n\n public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array\n {\n return [\n 'id' => 'period',\n 'label' => 'Select one-off period',\n 'inputType' => InputTypeEnum::DATE_RANGE,\n 'required' => true,\n 'placeholder' => 'Select',\n 'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],\n 'queryParams' => [\n 'startDate' => 'start_date_period',\n 'endDate' => 'end_date_period',\n ],\n 'dependencies' => [],\n 'dependsOn' => ['frequency'],\n ];\n }\n\n public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array\n {\n return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);\n }\n\n public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);\n }\n\n public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);\n }\n\n public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'deal_value',\n 'label' => 'Deal Value',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_deal_value',\n 'max' => 'max_deal_value',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array\n {\n $value = [];\n $conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;\n $dialerOn && $value[] = self::CALL_TYPE_DIALER;\n\n return [\n 'id' => 'call_type',\n 'label' => 'Call Type',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => [\n self::CALL_TYPE_CONFERENCE,\n self::CALL_TYPE_DIALER,\n ],\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getMediaTypeFieldData(?AutomatedReport $report = null): array\n {\n $value = [];\n\n if ($report) {\n $value = $this->transformMediaTypes($report);\n }\n\n return [\n 'id' => 'media_types',\n 'label' => 'Export as',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => self::MEDIA_TYPE_OBJECTS,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'call_duration',\n 'label' => 'Call Duration',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_call_duration',\n 'max' => 'max_call_duration',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getRecipientsFieldData(?Team $team = null, array $value = []): array\n {\n return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);\n }\n\n public function getJiminnyRecipientsFieldData(array $value = []): array\n {\n return $this->recipientsService->getJiminnyRecipientsFieldData($value);\n }\n\n public function getAdditionalPromptInputFieldData(?string $value = null): array\n {\n return [\n 'id' => 'additional_prompt_input',\n 'label' => 'Special requirements',\n 'inputType' => InputTypeEnum::TEXTAREA,\n 'required' => false,\n 'placeholder' => 'What should be the focus of the report?',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCustomReportNameFieldData(?string $value = null): array\n {\n return [\n 'id' => 'custom_name',\n 'label' => 'Custom report name',\n 'inputType' => InputTypeEnum::TEXT,\n 'required' => false,\n 'placeholder' => 'Enter custom name',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n // data providers\n public function getTeams(?Partner $partner = null): array\n {\n $teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);\n\n $teamData = [];\n foreach ($teams as $team) {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n continue;\n }\n\n $teamData[] = $this->transformTeam($team);\n }\n\n return $teamData;\n }\n\n public function getTeamGroups(string $teamUuid): array\n {\n $data = [];\n $team = $this->getTeam($teamUuid);\n\n if ($team !== null) {\n $groups = $team->groups()->get();\n\n foreach ($groups as $group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array\n {\n $data = [];\n $teams = $this->getTeams(partner: $partner);\n\n foreach ($teams as $team) {\n if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {\n continue;\n }\n\n $data[] = [\n 'label' => $team['name'],\n 'groups' => $this->getTeamGroups($team['id']),\n ];\n }\n\n return $data;\n }\n\n public function getTeam(string $teamUuid): ?Team\n {\n return $this->teamRepository->idOrUuid($teamUuid);\n }\n\n public function getTeamById(int $teamId): ?Team\n {\n return $this->teamRepository->find($teamId);\n }\n\n public function getGroupsUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportGroups = $report->getGroups();\n foreach ($reportGroups as $groupId) {\n if ($group = $this->groupRepository->find($groupId)) {\n $uuids[] = $group->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getPlaybookCategoriesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $playbookCategories = $report->getPlaybookCategories();\n foreach ($playbookCategories as $id) {\n if ($category = $this->playbookCategoryRepository->find($id)) {\n $uuids[] = $category->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getDealAtCallStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getDealAtCallStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getCurrentDealStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getCurrentDealStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getRecipients());\n }\n\n public function getJiminnyUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function extractUserUuids(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => $user->getUuid())\n ->values()\n ->all();\n }\n\n // get mail data\n public function getRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getRecipients());\n }\n\n /**\n * @return array<UserContract>\n */\n public function getRecipientUserObjects(AutomatedReport $report): array\n {\n $userIds = $report->getRecipients()['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->values()\n ->all();\n }\n\n private function getJiminnyRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function buildRecipientUsers(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => [\n 'email' => $user->getEmailAddress(),\n 'name' => $user->getName(),\n 'timezone' => $user->getTimezone()->getName(),\n ])\n ->values()\n ->all();\n }\n\n public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array\n {\n if ($report->isAskJiminnyReport()) {\n $recipients = $this->resolveAskJiminnyRecipients($report);\n } else {\n $recipients = $this->getRecipientUsers($report);\n if ($includeJiminny) {\n $recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));\n }\n }\n\n $emails = [];\n\n return array_values(array_filter(\n $recipients,\n static function ($recipient) use (&$emails) {\n if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {\n return false;\n }\n $emails[] = $recipient['email'];\n\n return true;\n }\n ));\n }\n\n private function resolveAskJiminnyRecipients(AutomatedReport $report): array\n {\n $recipients = [];\n\n $creator = $report->getCreator();\n if ($creator !== null) {\n $recipients[] = [\n 'email' => $creator->getEmailAddress(),\n 'name' => $creator->getName(),\n 'timezone' => $creator->getTimezone()->getName(),\n ];\n }\n\n return array_merge(\n $recipients,\n $this->buildRecipientUsers($report->getRecipients()),\n $this->getGroupRecipientUsers($report),\n );\n }\n\n private function getGroupRecipientUsers(AutomatedReport $report): array\n {\n $users = [];\n foreach ($report->getGroups() as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group === null) {\n continue;\n }\n foreach ($group->getMembers() as $member) {\n $users[] = [\n 'email' => $member->getEmailAddress(),\n 'name' => $member->getName(),\n 'timezone' => $member->getTimezone()->getName(),\n ];\n }\n }\n\n return $users;\n }\n\n public function getReportTypeName(AutomatedReportResult $report): string\n {\n $type = $report->getReport()->getType();\n\n $getType = $this->transformReportType($type);\n\n return $getType['name'];\n }\n\n public function getReportPeriodName(AutomatedReportResult $report): string\n {\n $from = $report->getFromDate();\n $to = $report->getToDate();\n $frequency = $report->getReport()->getFrequency();\n\n if ($from === null || $to === null) {\n if (! $report->getReport()->isAskJiminnyReport()) {\n $invalidPeriod = $from === null ? 'from' : 'to';\n\n throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);\n }\n\n $timezone = $report->getReport()->getCreator()?->getTimezone();\n $period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);\n $from = $period['fromDate'];\n $to = $period['toDate'];\n }\n\n return $this->formatReportPeriodName($frequency, $from, $to);\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 getReportTeamsName(AutomatedReportResult $report): string\n {\n $groups = $report->getGroups();\n\n if (empty($groups)) {\n return 'All';\n }\n\n // Get group names from repository\n $groupNames = [];\n foreach ($groups as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group) {\n $groupNames[] = $group->getName();\n }\n }\n\n if (count($groupNames) === 1) {\n // Single team format\n $teamsName = $groupNames[0];\n } else {\n // Multiple teams format\n $teamsName = implode(', ', $groupNames);\n }\n\n return $teamsName;\n }\n\n public function getReportFileName(AutomatedReportResult $report): string\n {\n $customName = $report->getReport()->getCustomName();\n $periodName = $this->getReportPeriodName($report);\n $filenameSuffix = $this->getFilenameSuffix($report);\n\n if ($customName) {\n if ($filenameSuffix) {\n $customName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$customName} - {$periodName}\");\n }\n\n $baseName = $this->getReportTypeName($report);\n\n if ($filenameSuffix) {\n $baseName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}\");\n }\n\n public function getReportFileNameWithExtension(AutomatedReportResult $result): string\n {\n $extension = $this->getMediaTypeMetadata($result)['extension'];\n\n return $this->getReportFileName($result) . '.' . $extension;\n }\n\n public function sanitizeFileName(string $fileName): string\n {\n return str_replace(['/', '\\\\'], '-', $fileName);\n }\n\n public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool\n {\n $recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);\n if (in_array($user->getId(), $recipientIds, true)) {\n return true;\n }\n\n if ($report->isAskJiminnyReport()) {\n $groupId = $user->getGroupId();\n if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {\n return true;\n }\n }\n\n return false;\n }\n\n public function transformReportResults(Collection $automatedReportResults): array\n {\n $data = [];\n foreach ($automatedReportResults as $automatedReportResult) {\n /** @var AutomatedReportResult $automatedReportResult */\n\n $report = $automatedReportResult->getReport();\n\n $createdBy = $report->getCreator();\n $creator = [\n 'id' => $createdBy?->getUuid(),\n 'name' => $createdBy?->getName(),\n 'email' => $createdBy?->getEmailAddress(),\n 'photoUrl' => $createdBy?->getPhotoUrl(),\n ];\n\n $data[] = [\n 'id' => $automatedReportResult->getUuid(),\n 'name' => $automatedReportResult->getName(),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n 'recipients' => $this->buildRecipients($report),\n 'report_type' => $this->transformReportType($report->getType()),\n 'media_type' => $automatedReportResult->getMediaType(),\n 'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),\n 'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),\n 'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),\n 'creator' => $creator,\n ];\n }\n\n return $data;\n }\n\n private function buildRecipients(AutomatedReport $report): array\n {\n $creatorUuid = $report->getCreator()?->getUuid();\n\n $recipients = array_values(array_filter(\n $this->transformRecipients($report->getRecipients()),\n static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,\n ));\n\n if (! $report->isAskJiminnyReport()) {\n return $recipients;\n }\n\n return [\n ...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),\n ...$recipients,\n ];\n }\n\n public function hasCallTypeConference(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);\n }\n\n public function hasCallTypeDialer(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);\n }\n\n // transformers\n private function transformTeam(Team $team): array\n {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n return [];\n }\n\n return [\n 'id' => $team->getUuid(),\n 'name' => $team->getName(),\n ];\n }\n\n private function transformReportFullView(AutomatedReport $report): array\n {\n $base = $this->transformReportBase($report);\n\n return $report->getType() === self::TYPE_ASK_JIMINNY\n ? $base + $this->transformAskJiminnyFields($report)\n : $base + $this->transformStandardReportFields($report);\n }\n\n private function transformReportBase(AutomatedReport $report): array\n {\n return [\n 'id' => $report->getUuid(),\n 'organization' => $this->transformOrganization(team: $report->getTeam()),\n 'report_type' => $this->transformReportType($report->getType()),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n ];\n }\n\n private function transformStandardReportFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n\n return [\n 'report_enabled' => $report->getStatus(),\n 'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),\n 'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),\n 'deal_value_min' => $report->getDealValueMin(),\n 'deal_value_max' => $report->getDealValueMax(),\n 'call_types' => $this->transformCallType($report->getCallTypes()),\n 'media_types' => $this->transformMediaTypes($report),\n 'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),\n 'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),\n 'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),\n 'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),\n 'recipients' => $this->transformRecipients($report->getRecipients()),\n 'created_by' => $this->transformCreator($report->getCreator()),\n 'additional_prompt_input' => $report->getAdditionalPromptInput(),\n 'custom_name' => $report->getCustomName(),\n 'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),\n 'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),\n 'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),\n ];\n }\n\n private function transformAskJiminnyFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n $creatorId = $report->getAttribute('created_by');\n $explicitUserIds = array_values(array_filter(\n $report->getRecipients()['users'] ?? [],\n static fn ($id) => $id !== $creatorId\n ));\n\n return [\n 'report_name' => $report->getCustomName(),\n 'enabled' => $report->getStatus(),\n 'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),\n 'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),\n 'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),\n 'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),\n ];\n }\n\n private function transformOrganization(?Team $team): array\n {\n return [\n 'id' => $team?->getUuid(),\n 'name' => $team?->getName(),\n ];\n }\n\n private function transformReportType(string $type): array\n {\n foreach (self::ALL_TYPES as $typeItem) {\n if ($typeItem['id'] === $type) {\n return $typeItem;\n }\n }\n\n return [];\n }\n\n private function transformCallType(array $types): array\n {\n $result = [];\n $callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];\n\n foreach ($types as $type) {\n foreach ($callTypes as $callTypeItem) {\n if ($callTypeItem['id'] === $type) {\n $result[] = $callTypeItem;\n\n break;\n }\n }\n }\n\n return $result;\n }\n\n private function transformMediaTypes(AutomatedReport $report): array\n {\n $values = [];\n\n foreach ($report->getMediaTypes() as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n continue;\n }\n\n $values[] = match ($mediaType) {\n self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,\n self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,\n };\n }\n\n return $values;\n }\n\n private function transformFrequency(string $frequency): array\n {\n foreach (self::ALL_FREQUENCIES as $frequencyItem) {\n if ($frequencyItem['id'] === $frequency) {\n return $frequencyItem;\n }\n }\n\n return [];\n }\n\n public function transformDurationToMinutes(?int $duration): ?int\n {\n if (! $duration) {\n return null;\n }\n\n return (int) ($duration / 60);\n }\n\n private function transformGroups(?Team $team, array $groupsIds): array\n {\n if (empty($groupsIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($groupsIds as $groupId) {\n $group = $team->groups()->where('id', $groupId)->first();\n\n if ($group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n 'photoUrl' => $group->getPhotoUrl(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformStages(?Team $team, array $stagesIds): array\n {\n if (empty($stagesIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($stagesIds as $stageId) {\n $stage = $team->stages()->where('id', $stageId)->first();\n\n if ($stage) {\n $data[] = [\n 'id' => $stage->getUuid(),\n 'name' => $stage->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformRecipients(array $recipients): array\n {\n $users = [];\n foreach ($recipients['users'] ?? [] as $userId) {\n $users[] = $this->transformUser($userId);\n }\n\n return $users;\n }\n\n private function transformCreator(?User $user): ?array\n {\n if ($user === null) {\n return null;\n }\n\n return $this->transformUser($user->getId());\n }\n\n private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array\n {\n if ($prompt === null) {\n return null;\n }\n\n return [\n 'id' => $prompt->getUuid(),\n 'name' => $prompt->getTitle(),\n ];\n }\n\n private function transformSafeSearch(?Search $search): ?array\n {\n if ($search === null) {\n return null;\n }\n\n return [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ];\n }\n\n private function transformUser(int $userId): array\n {\n /* @var ?User $user */\n $user = $this->userRepository->find($userId);\n\n return [\n 'id' => $user?->getUuid(),\n 'name' => $user?->getName(),\n 'email' => $user?->getEmailAddress(),\n 'photoUrl' => $user?->getPhotoUrl(),\n ];\n }\n\n public function create(array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $validatedData['created_by'] = auth()->id();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function update(string $uuid, array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $report = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $report) {\n throw new InvalidArgumentException('Report not found');\n }\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Create an Ask Jiminny report.\n */\n public function createAskJiminnyReport(array $data, User $creator): array\n {\n $validatedData = $this->validateAskJiminnyReportData($data, $creator);\n $validatedData['created_by'] = $creator->getId();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Update an Ask Jiminny report.\n */\n public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array\n {\n if (! $report->isAskJiminnyReport()) {\n throw new InvalidArgumentException('Report is not an Ask Jiminny report');\n }\n\n $validatedData = $this->validateAskJiminnyReportData($data, $user);\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array\n {\n if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {\n throw new InvalidArgumentException(\n 'This report is missing a saved search or prompt. ' .\n 'Edit the report to complete the setup before enabling it.'\n );\n }\n\n $this->automatedReportsRepository->update($report, ['status' => $status]);\n\n return $this->transformReportFullView($report->fresh());\n }\n\n /**\n * Validate and transform data for Ask Jiminny reports.\n */\n private function validateAskJiminnyReportData(array $data, User $user): array\n {\n // Validate name\n $name = trim($data['report_name'] ?? '');\n if (empty($name)) {\n throw new InvalidArgumentException('Report name is required');\n }\n if (mb_strlen($name) > 50) {\n throw new InvalidArgumentException('Report name must be 50 characters or less');\n }\n\n // Validate frequency (only daily, weekly, monthly for Ask Jiminny)\n $frequency = $data['frequency'] ?? null;\n $askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];\n if (! in_array($frequency, $askJiminnyFrequencies, true)) {\n throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');\n }\n\n // Validate expiration date\n $expiresAt = $data['expires_on'] ?? null;\n if (empty($expiresAt)) {\n throw new InvalidArgumentException('Expiration date is required');\n }\n\n try {\n $expiresAtDate = Carbon::parse($expiresAt);\n } catch (InvalidFormatException $e) {\n throw new InvalidArgumentException('Expiration date format is invalid');\n }\n $maxExpiration = Carbon::now()->addYear()->endOfDay();\n if ($expiresAtDate->gt($maxExpiration)) {\n throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');\n }\n if ($expiresAtDate->isPast()) {\n throw new InvalidArgumentException('Expiration date cannot be in the past');\n }\n\n // Validate saved search\n $activitySearchId = $data['saved_search'] ?? null;\n if (empty($activitySearchId)) {\n throw new InvalidArgumentException('Saved search is required');\n }\n $savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);\n if (! $savedSearch) {\n throw new InvalidArgumentException('Saved search not found or does not belong to you');\n }\n\n // Validate saved prompt\n $askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;\n if (empty($askAnythingPromptId)) {\n throw new InvalidArgumentException('Ask Jiminny prompt is required');\n }\n $prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);\n if (! $prompt) {\n throw new InvalidArgumentException('Ask Jiminny prompt not found');\n }\n\n // Validate status\n $status = $data['enabled'] ?? false;\n\n $recipientUserIds = [$user->getId()];\n\n if (! empty($data['share_users'])) {\n $sharedUserIds = $this->validateAndGetUserIdsByTeam(\n $user->team,\n (array) $data['share_users']\n );\n $recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);\n }\n\n $sharedGroupIds = [];\n if (! empty($data['share_teams'])) {\n $sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);\n }\n\n $recipientUserIds = array_values(array_unique($recipientUserIds));\n\n return [\n 'team_id' => $user->getTeamId(),\n 'type' => self::TYPE_ASK_JIMINNY,\n 'status' => (bool) $status,\n 'frequency' => $frequency,\n 'custom_name' => $name,\n 'activity_search_id' => $savedSearch->getId(),\n 'ask_anything_prompt_id' => $prompt->getId(),\n 'expires_at' => $expiresAtDate->toDateString(),\n 'media_types' => [self::MEDIA_TYPE_PDF],\n 'call_types' => [],\n 'recipients' => ['users' => $recipientUserIds],\n 'groups' => $sharedGroupIds,\n ];\n }\n\n public static function getAskJiminnyFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::ASK_JIMINNY_FREQUENCIES);\n }\n\n public function getAskJiminnyReportFilters(User $user): array\n {\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n return [\n [\n 'id' => 'prompt',\n 'label' => 'Prompt',\n 'options' => $prompts,\n ],\n [\n 'id' => 'saved_search',\n 'label' => 'Saved Search',\n 'options' => $savedSearches,\n ],\n ];\n }\n\n public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array\n {\n $team = $user->getTeam();\n $userTimezone = $user->getTimezone();\n\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n $teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ])->values()->all();\n\n $shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];\n\n $sharedTeamsValue = [];\n $sharedUsersValue = [];\n if ($report) {\n $sharedTeamsValue = $this->transformGroups($team, $report->getGroups());\n\n $recipientUserIds = $report->getRecipients()['users'] ?? [];\n $creatorId = $report->getAttribute('created_by');\n $sharedUserIds = array_values(array_filter(\n $recipientUserIds,\n static fn ($id) => $id !== $creatorId\n ));\n $sharedUsersValue = collect($sharedUserIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (User $u) => [\n 'id' => $u->getUuid(),\n 'name' => $u->getName(),\n ])\n ->values()\n ->all();\n }\n\n return [\n 'fields' => [\n [\n 'id' => 'enabled',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'label' => '',\n 'value' => $report?->getStatus() ?? false,\n ],\n [\n 'id' => 'report_name',\n 'inputType' => InputTypeEnum::TEXT,\n 'label' => 'Name',\n 'placeholder' => 'Enter name',\n 'required' => true,\n 'validation' => ['maxLength' => 50],\n 'value' => $report?->getCustomName() ?? '',\n ],\n [\n 'id' => 'frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Frequency',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::ASK_JIMINNY_FREQUENCIES,\n 'value' => $report ? $this->transformFrequency($report->getFrequency()) : null,\n ],\n [\n 'id' => 'expires_on',\n 'inputType' => InputTypeEnum::DATE,\n 'label' => 'Expires on',\n 'required' => true,\n 'placeholder' => 'Select',\n 'validation' => [\n 'minDate' => now($userTimezone)->toDateString(),\n 'maxDate' => now($userTimezone)->addYear()->toDateString(),\n ],\n 'value' => $report?->getExpiresAt()?->toDateString(),\n ],\n [\n 'id' => 'share_teams',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team',\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $teamGroups,\n 'value' => $sharedTeamsValue,\n ],\n [\n 'id' => 'share_users',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team member',\n 'required' => false,\n 'placeholder' => 'Select',\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'options' => $shareUsers,\n 'value' => $sharedUsersValue,\n ],\n [\n 'id' => 'saved_search',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Saved search',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $savedSearches,\n 'value' => $report && $report->getSavedSearch() ? [\n 'id' => $report->getSavedSearch()->getUuid(),\n 'name' => $report->getSavedSearch()->getName(),\n ] : null,\n ],\n [\n 'id' => 'ask_jiminny_prompt',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Ask Jiminny prompt',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $prompts,\n 'value' => $report && $report->getAskAnythingPrompt() ? [\n 'id' => $report->getAskAnythingPrompt()->getUuid(),\n 'name' => $report->getAskAnythingPrompt()->getTitle(),\n ] : null,\n ],\n ],\n ];\n }\n\n private function updateResultNames(AutomatedReport $automatedReport): void\n {\n $results = $this->automatedReportsRepository->getResultsByReport($automatedReport);\n\n foreach ($results as $result) {\n $result->update(['name' => $this->getReportFileName($result)]);\n }\n }\n\n public function updateStatus(string $uuid, array $data): array\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $automatedReport->update([\n 'status' => $status,\n ]);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n private function generateOneOffReport(AutomatedReport $automatedReport): void\n {\n // the scheduler handles all the other frequency types\n if ($automatedReport->getStatus() === false || $automatedReport->getFrequency() !== self::FREQUENCY_ONE_OFF) {\n return;\n }\n\n $this->dispatcher->dispatch(new RequestGenerateReportJob($automatedReport->getUuid()));\n }\n\n public function getReport(string $uuid, ?Partner $partner = null): AutomatedReport\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n if ($partner !== null && ! $partner->isDefaultPartner() && $automatedReport->team->partner_id !== $partner->getId()) {\n throw new ModelNotFoundException('Report not found');\n }\n\n return $automatedReport;\n }\n\n public function get(string $uuid, ?Partner $partner = null): array\n {\n $automatedReport = $this->getReport($uuid, $partner);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function list(string $sortColumn = 'created_at', string $sortDirection = 'desc', ?Partner $partner = null): array\n {\n $results = [];\n $collection = $this->automatedReportsRepository->getAllStandardReports($sortColumn, $sortDirection, $partner);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function listAskJiminnyReports(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): array {\n $results = [];\n $collection = $this->automatedReportsRepository->getAskJiminnyReportsByUser($user, $sortColumn, $sortDirection);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function delete(string $uuid): void\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $automatedReport->delete();\n }\n\n public function createReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n return $this->automatedReportsRepository->createResult(\n array_merge(\n [\n 'report_id' => $automatedReport->getId(),\n 'status' => AutomatedReportResult::STATUS_DEFAULT,\n ],\n $data\n )\n );\n }\n\n public function getOrCreateReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n $existing = $this->automatedReportsRepository->findLatestSameDayDefaultOrFailedResult($automatedReport);\n\n if ($existing !== null) {\n $existing->update(['status' => AutomatedReportResult::STATUS_DEFAULT]);\n\n return $existing;\n }\n\n return $this->createReportResult($automatedReport, $data);\n }\n\n public function getReportResult(string $resultUuid): AutomatedReportResult\n {\n $report = $this->automatedReportsRepository->findResultByUuid($resultUuid);\n\n if (! $report) {\n throw new ModelNotFoundException('Report Result not found');\n }\n\n return $report;\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return $this->automatedReportsRepository->findChildResult($result, $type);\n }\n\n // prophet API calls\n /**\n * @throws ApplicationException\n */\n public function getGenerateReportPayload(AutomatedReport $automatedReport, string $reportResultUuid): array\n {\n $period = $this->calculateFromAndToDate($automatedReport);\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n return [\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResultUuid,\n 'report_type' => $automatedReport->getType(),\n 'media_types' => $automatedReport->getMediaTypes(),\n 'from_date' => $fromDate->startOfDay()->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->endOfDay()->format(DateTimeInterface::RFC3339),\n 'group_ids' => $automatedReport->getGroups(),\n 'call_deal_stage' => $automatedReport->getDealAtCallStages(),\n 'current_deal_stage' => $automatedReport->getCurrentDealStages(),\n 'deal_min_value' => $automatedReport->getDealValueMin(),\n 'deal_max_value' => $automatedReport->getDealValueMax(),\n 'call_types' => $automatedReport->getCallTypes(),\n 'call_duration_min_seconds' => $automatedReport->getCallDurationMin(),\n 'call_duration_max_seconds' => $automatedReport->getCallDurationMax(),\n 'special_requirements' => $automatedReport->getAdditionalPromptInput(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->formatReportPeriodName(\n $automatedReport->getFrequency(),\n $fromDate,\n $toDate,\n ),\n 'playbook_categories' => $automatedReport->getPlaybookCategories(),\n 'custom_name' => $automatedReport->getCustomName(),\n ];\n }\n\n // $inputPayload - FE payload structure\n public function getActivitiesCountPayload(array $inputPayload): array\n {\n // Use validateAndTransformData to validate and normalize input\n $validatedData = $this->validateAndTransformData($inputPayload);\n $period = $this->calculateFromAndToDatePeriod(\n $validatedData['frequency'],\n Carbon::parse($validatedData['from']),\n Carbon::parse($validatedData['to']),\n );\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n // Create payload similar to getGenerateReportPayload\n return [\n 'team_id' => $validatedData['team_id'],\n 'group_ids' => $validatedData['groups'] ?? [],\n 'report_type' => $validatedData['type'],\n 'from_date' => $fromDate->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->format(DateTimeInterface::RFC3339),\n 'call_deal_stage' => $validatedData['deal_at_call_stages'] ?? [],\n 'current_deal_stage' => $validatedData['current_deal_stages'] ?? [],\n 'deal_min_value' => $validatedData['deal_value_min'] ?? null,\n 'deal_max_value' => $validatedData['deal_value_max'] ?? null,\n 'call_types' => $validatedData['call_types'],\n 'call_duration_min_seconds' => $validatedData['call_duration_min'] ?? null,\n 'call_duration_max_seconds' => $validatedData['call_duration_max'] ?? null,\n 'special_requirements' => $validatedData['additional_prompt_input'] ?? null,\n 'playbook_categories' => $validatedData['playbook_categories'] ?? [],\n 'request_id' => null,\n 'callback_url' => null,\n ];\n }\n\n public function shouldSendReport(array $users, ?CarbonInterface $generatedAt = null): bool\n {\n if (empty($users)) {\n return false;\n }\n\n $earliestTz = collect($users)\n ->mapWithKeys(function (array $user) {\n $tz = new DateTimeZone($user['timezone']);\n $nowUtc = new DateTime('now', new DateTimeZone('UTC'));\n $offset = $tz->getOffset($nowUtc);\n\n return [$user['timezone'] => $offset];\n })\n ->sortDesc()\n ->keys()\n ->first();\n\n $now = Carbon::now($earliestTz);\n $isScheduledTime = (int) $now->format('H') === self::SENT_REPORT_AT_HOURS;\n\n if ($isScheduledTime) {\n return true;\n }\n\n return $this->hasPassedScheduledTime($generatedAt, $earliestTz);\n }\n\n public function hasPassedScheduledTime(?CarbonInterface $generatedAt, string $timezone): bool\n {\n if ($generatedAt === null) {\n return false;\n }\n\n $now = Carbon::now($timezone);\n $scheduledTime = $now->copy()->setTime(self::SENT_REPORT_AT_HOURS, 0, 0);\n\n if ($now->hour < self::SENT_REPORT_AT_HOURS) {\n $scheduledTime = $scheduledTime->subDay();\n }\n\n $scheduledTimeUtc = $scheduledTime->copy()->utc();\n $generatedAtUtc = $generatedAt->copy()->utc();\n $nowUtc = $now->copy()->utc();\n\n return $generatedAtUtc->lt($scheduledTimeUtc) && $nowUtc->gt($scheduledTimeUtc);\n }\n\n public function calculateFromAndToDatePeriod(\n string $frequency,\n ?Carbon $fromDate = null,\n ?Carbon $toDate = null,\n DateTimeZone|string|null $timezone = 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($timezone);\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()->subWeek()->startOfWeek(CarbonInterface::MONDAY),\n 'toDate' => $now->copy()->subWeek()->endOfWeek(CarbonInterface::SUNDAY),\n ],\n self::FREQUENCY_MONTHLY => [\n 'fromDate' => $now->copy()->subMonthNoOverflow()->startOfMonth(),\n 'toDate' => $now->copy()->subMonthNoOverflow()->endOfMonth(),\n ],\n self::FREQUENCY_QUARTERLY => [\n 'fromDate' => $now->copy()->subQuarterNoOverflow()->startOfQuarter(),\n 'toDate' => $now->copy()->subQuarterNoOverflow()->endOfQuarter(),\n ],\n default => throw new InvalidArgumentException(\"Unsupported frequency: {$frequency}\"),\n };\n }\n\n private function calculateFromAndToDate(AutomatedReport $automatedReport): array\n {\n return $this->calculateFromAndToDatePeriod(\n $automatedReport->getFrequency(),\n $automatedReport->getFrom(),\n $automatedReport->getTo()\n );\n }\n\n public function getAskJiminnyGenerateReportPayload(\n AutomatedReport $automatedReport,\n AutomatedReportResult $reportResult,\n array $activityIds,\n ): array {\n return [\n 'user_question' => $automatedReport->getAskAnythingPrompt()?->getContent(),\n 'call_ids' => array_map('strval', $activityIds),\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResult->getUuid(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->getReportPeriodName($reportResult),\n 'report_name' => $automatedReport->getCustomName(),\n ];\n }\n\n private function getCallbackUrl(): string\n {\n return $this->webhookService->route('jiminny.webhook.reports.ready');\n }\n\n /**\n * Validate and transform payload data for automated reports\n *\n * @param array $data\n *\n * @throws InvalidArgumentException\n *\n * @return array\n */\n private function validateAndTransformData(array $data): array\n {\n // Validate organization (team) and check feature\n $team = $this->validateOrganization($data['organization'] ?? null);\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $type = $this->validateReportType($data['report_type'] ?? null);\n $frequency = $this->validateFrequency($data['frequency'] ?? null);\n $additionalPromptInput = $this->validateAdditionalPromptInput(\n $data['additional_prompt_input'] ?? null\n );\n $customReportName = $this->validateCustomReportName($data['custom_name'] ?? null);\n\n // Prepare data for the database\n $reportData = [\n 'team_id' => $team->getId(),\n 'type' => $type,\n 'status' => $status,\n 'frequency' => $frequency,\n 'additional_prompt_input' => $additionalPromptInput,\n 'custom_name' => $customReportName,\n ];\n\n // Validate deal values\n $reportData = $this->validateDealValues($data, $reportData);\n\n // Validate date range\n $reportData = $this->validateDateRange($data, $reportData, $frequency);\n\n // Validate call durations\n $reportData = $this->validateCallDurations($data, $reportData);\n\n // Validate call types\n $reportData = $this->validateCallTypes($data, $reportData);\n\n // Validate media types\n $reportData = $this->validateMediaTypes($data, $reportData);\n\n // Validate groups\n if (isset($data['teams'])) {\n $reportData['groups'] = $this->validateAndGetGroupIds($team, $data['teams']);\n }\n\n // Validate deal stages\n $reportData = $this->validateDealStages($data, $reportData, $team, $type);\n\n // Validate playbook categories\n $reportData = $this->validatePlaybookCategories($data, $reportData, $team);\n\n // Validate recipients\n $reportData['recipients'] = [\n 'users' => $this->validateAndGetUserIdsByTeam($team, $data['recipients'] ?? []),\n ];\n\n if (isset($data['jiminny_recipients'])) {\n // Validate Jiminny recipients\n $reportData['jiminny_recipients'] = [\n 'users' => $this->validateAndGetJiminnyUserIds((array) $data['jiminny_recipients']),\n ];\n }\n\n return $reportData;\n }\n\n private function validateDealValues(array $data, array $reportData): array\n {\n if (isset($data['min_deal_value'])) {\n $reportData['deal_value_min'] = (int) $data['min_deal_value'];\n\n if ($reportData['deal_value_min'] > 4294967295 || $reportData['deal_value_min'] < 0) {\n throw new InvalidArgumentException('Min deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_deal_value'])) {\n $reportData['deal_value_max'] = (int) $data['max_deal_value'];\n\n if ($reportData['deal_value_max'] > 4294967295 || $reportData['deal_value_max'] < 0) {\n throw new InvalidArgumentException('Max deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['min_deal_value'], $data['max_deal_value'])\n && $data['min_deal_value'] > $data['max_deal_value']\n ) {\n throw new InvalidArgumentException('Min deal value cannot be greater than max deal value');\n }\n\n return $reportData;\n }\n\n private function validateDateRange(array $data, array $reportData, string $frequency): array\n {\n // Set date range only for one_off frequency\n if ($frequency === 'one_off') {\n if (isset($data['start_date_period'])) {\n $reportData['from'] = $this->parseDate($data['start_date_period']);\n }\n\n if (isset($data['end_date_period'])) {\n $reportData['to'] = $this->parseDate($data['end_date_period']);\n }\n\n if (empty($reportData['from']) || empty($reportData['to'])) {\n throw new InvalidArgumentException(\n 'Start date and end date are required for one_off frequency'\n );\n }\n } else {\n $reportData['from'] = null;\n $reportData['to'] = null;\n }\n\n return $reportData;\n }\n\n private function validateCallDurations(array $data, array $reportData): array\n {\n // Convert call durations from minutes to seconds\n if (isset($data['min_call_duration'])) {\n $reportData['call_duration_min'] = (int) $data['min_call_duration'] * 60;\n\n if ($reportData['call_duration_min'] > 4294967295 || $reportData['call_duration_min'] < 0) {\n throw new InvalidArgumentException('Min call duration should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_call_duration'])) {\n $reportData['call_duration_max'] = (int) $data['max_call_duration'] * 60;\n\n if ($reportData['call_duration_max'] > 4294967295 || $reportData['call_duration_max'] < 0) {\n throw new InvalidArgumentException('Max call duration should be between 0 and 4294967295');\n }\n }\n\n return $reportData;\n }\n\n private function validateCallTypes(array $data, array $reportData): array\n {\n // Set call types\n $reportData['call_types'] = $data['call_type'] ?? [];\n if (empty($reportData['call_types'])) {\n $reportData['call_types'] = self::getCallTypes();\n }\n\n foreach ($reportData['call_types'] as $callType) {\n if (! in_array($callType, self::getCallTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Call type %s is invalid', $callType));\n }\n }\n\n return $reportData;\n }\n\n private function validateMediaTypes(array $data, array $reportData): array\n {\n // Set media types from input data\n $reportData['media_types'] = $data['media_types'] ?? [];\n\n if (empty($reportData['media_types'])) {\n throw new InvalidArgumentException('Media types are required');\n }\n\n foreach ($reportData['media_types'] as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n throw new InvalidArgumentException(sprintf('Media type %s is invalid', $mediaType));\n }\n }\n\n return $reportData;\n }\n\n private function validateDealStages(array $data, array $reportData, Team $team, string $reportType): array\n {\n // Validate and set deal stages\n if (isset($data['deal_stage_at_call'])) {\n $reportData['deal_at_call_stages'] =\n $this->validateAndGetDealStageIds($team, $data['deal_stage_at_call'], 'Deal stage at call');\n }\n\n if (isset($data['current_deal_stage'])) {\n $reportData['current_deal_stages'] =\n $this->validateAndGetDealStageIds($team, $data['current_deal_stage'], 'Current deal stage');\n }\n\n // Ensure current_deal_stage is not provided for loss_analysis report type\n if ($reportType === self::TYPE_LOSS_ANALYSIS && ! empty($data['current_deal_stage'])) {\n throw new InvalidArgumentException('Current deal stage is not applicable for Loss Analysis reports');\n }\n\n return $reportData;\n }\n\n // transform uuid to id\n private function validatePlaybookCategories(array $data, array $reportData, Team $team): array\n {\n $key = 'playbook_categories';\n\n if (isset($data[$key])) {\n $payloadIds = $data[$key];\n $ids = [];\n\n foreach ($payloadIds as $uuid) {\n $uuid = (string) $uuid;\n\n try {\n $playbookCategory = $this->playbookCategoryRepository->findByUuid($uuid);\n } catch (Throwable $throwable) {\n Log::error(__METHOD__ . ' ' . $throwable->getMessage());\n\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory) {\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory->hasPlaybook()) {\n throw new InvalidArgumentException(sprintf('Playbook category %s has no playbook', $uuid));\n }\n\n if ($playbookCategory->getPlaybook()->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Playbook category %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $ids[] = $playbookCategory->getId();\n }\n\n $reportData[$key] = $ids;\n }\n\n return $reportData;\n }\n\n private function validateReportStatus($status): bool\n {\n if (! in_array($status, [true, false], true)) {\n throw new InvalidArgumentException('Report status is invalid');\n }\n\n return $status;\n }\n\n private function validateReportType($type): string\n {\n if (! in_array($type, self::getTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Report type is invalid: %s', $type));\n }\n\n return $type;\n }\n\n private function validateFrequency($frequency): string\n {\n if (! in_array($frequency, self::getFrequencies(), true)) {\n throw new InvalidArgumentException('Frequency is invalid');\n }\n\n return $frequency;\n }\n\n private function validateAdditionalPromptInput(?string $additionalPromptInput): ?string\n {\n if ($additionalPromptInput && strlen($additionalPromptInput) > 5000) {\n throw new InvalidArgumentException('Additional Prompt Input should be less than 5000 characters');\n }\n\n return $additionalPromptInput;\n }\n\n private function validateCustomReportName(?string $customReportName): ?string\n {\n if ($customReportName === null || $customReportName === '') {\n return null;\n }\n\n if (strlen($customReportName) > 70) {\n throw new InvalidArgumentException('Custom report name should be less than 70 characters');\n }\n\n return $customReportName;\n }\n\n private function validateOrganization(?string $organizationUuid): Team\n {\n if (! $organizationUuid) {\n throw new InvalidArgumentException('Organization is required');\n }\n\n $team = $this->teamRepository->idOrUuid($organizationUuid);\n\n if (! $team) {\n throw new InvalidArgumentException('Organization not found');\n }\n\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n throw new InvalidArgumentException('Organization does not have the Automated Reports feature');\n }\n\n return $team;\n }\n\n private function validateAndGetGroupIds(Team $team, array $teamUuids): array\n {\n $groupIds = [];\n\n foreach ($teamUuids as $uuid) {\n $group = $this->groupRepository->findByUuid($uuid);\n\n if ($group === null || $group->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Group %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $groupIds[] = $group->getId();\n\n }\n\n return $groupIds;\n }\n\n private function validateAndGetDealStageIds(Team $team, array $stageUuids, string $propertyLabel): array\n {\n $stageIds = [];\n\n foreach ($stageUuids as $uuid) {\n $stage = $this->stageRepository->findByUuid($uuid);\n\n if ($stage === null || $stage->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Stage %s not found for team %s for %s', $uuid, $team->getUuid(), $propertyLabel)\n );\n }\n\n $stageIds[] = $stage->getId();\n }\n\n return $stageIds;\n }\n\n private function validateAndGetUserIds(array $userUuids, callable $teamCheck): array\n {\n if (empty($userUuids)) {\n return [];\n }\n\n $userIds = [];\n\n foreach ($userUuids as $uuid) {\n $user = $this->userRepository->findByUuid($uuid);\n\n if (! $user || ! $user->isStatusActive()) {\n throw new InvalidArgumentException(\n sprintf('User %s not found or is not active', $uuid)\n );\n }\n\n if (! $teamCheck($user)) {\n throw new InvalidArgumentException(\n sprintf('User %s does not belong to the allowed team(s)', $uuid)\n );\n }\n\n $userIds[] = $user->getId();\n }\n\n return $userIds;\n }\n\n private function validateAndGetUserIdsByTeam(Team $team, array $userUuids): array\n {\n return $this->validateAndGetUserIds($userUuids, fn ($user) => $user->getTeamId() === $team->getId());\n }\n\n private function validateAndGetJiminnyUserIds(array $userUuids): array\n {\n $allowedTeamIds = config('kiosk.teamIds', []);\n\n return $this->validateAndGetUserIds($userUuids, fn ($user) => in_array($user->getTeamId(), $allowedTeamIds, true));\n }\n\n private function parseDate(string $dateString): string\n {\n return date('Y-m-d H:i:s', strtotime($dateString));\n }\n\n private function generateReportResultViewUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.view', ['uuid' => $result->getUuid()]);\n }\n\n private function generateReportResultDownloadUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.download', ['uuid' => $result->getUuid()]);\n }\n\n private function getReportMediaRouteResource(AutomatedReportResult $result): string\n {\n if ($result->getMediaType() === self::MEDIA_TYPE_PDF) {\n return self::PDF_KEY;\n } elseif ($result->getMediaType() === self::MEDIA_TYPE_PODCAST) {\n return self::AUDIO_KEY;\n }\n\n throw new \\InvalidArgumentException('Unknown media type.');\n }\n\n public function getMediaPath(AutomatedReportResult $result): ?string\n {\n $url = match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => $result->getPdfUrl(),\n self::MEDIA_TYPE_PODCAST => $result->getPodcastAudioUrl(),\n default => null,\n };\n\n if ($url === null) {\n return null;\n }\n\n $path = parse_url(trim($url, '\"\\''), PHP_URL_PATH);\n\n return $path ?: null;\n }\n\n public function getFilenameSuffix(AutomatedReportResult $result): ?string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => 'Podcast',\n default => null,\n };\n }\n\n public function getMailSubjectSuffix(AutomatedReportResult $result): string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => 'report',\n self::MEDIA_TYPE_PODCAST => 'podcast',\n default => '',\n };\n }\n\n public function getMediaTypeMetadata(AutomatedReportResult $result): array\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => ['extension' => 'mp3', 'mime' => 'audio/mpeg'],\n self::MEDIA_TYPE_PDF => ['extension' => 'pdf', 'mime' => 'application/pdf'],\n default => ['extension' => null, 'mime' => null],\n };\n }\n\n public function deleteS3Files(AutomatedReportResult $result): void\n {\n $teamUuid = $result->getReport()->getTeam()->getUuid();\n $reportUuid = $result->getUuid();\n\n // delete all files for a report uuid no mather of pdf, podcast, or both\n // in case of both - the podcast files are linked to the pdf (parent) uuid\n // pdf and podcast date times should be close\n $path = sprintf('%s/%s/%s', $teamUuid, self::S3_DIR, $reportUuid);\n\n foreach (self::FILE_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted S3 file', [\n 'path' => $file,\n ]);\n }\n }\n\n foreach (self::FILE_PODCAST_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '_podcast.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted Podcast S3 file', [\n 'path' => $file,\n ]);\n }\n }\n }\n\n /**\n *\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): Collection\n {\n return $this->automatedReportsRepository->getTeamIdsWithReportsResults($teamId);\n }\n\n /**\n * Core delete logic for report results using a query\n *\n * @param Builder $query\n * @param array $logContext\n *\n * @return int\n */\n private function deleteReportResultsByQuery(Builder $query, array $logContext = []): int\n {\n $deletedCount = 0;\n\n if ($query->exists()) {\n Log::info(\n 'Run delete report results',\n array_merge(\n $logContext,\n [\n 'service' => 'AutomatedReportsService',\n ]\n )\n );\n\n $query->chunkById(50, function ($results) use (&$deletedCount, $logContext) {\n foreach ($results as $result) {\n $this->deleteReportResult($result);\n $deletedCount++;\n\n Log::info(\n 'Deleted a report result',\n array_merge(\n $logContext,\n [\n 'result_id' => $result->getId(),\n 'report_id' => $result->getReportId(),\n ]\n )\n );\n }\n });\n }\n\n return $deletedCount;\n }\n\n /**\n * Delete report results for a team by retention period\n *\n * @param Team $team\n * @param CarbonImmutable $retentionDate\n *\n * @return int Number of deleted report results\n */\n public function deleteReportsResultsInRetentionPeriod(Team $team, CarbonImmutable $retentionDate): int\n {\n $reportIds = $this->automatedReportsRepository->getReportIdsByTeam($team);\n\n if ($reportIds->isEmpty()) {\n return 0;\n }\n\n $query = $this->automatedReportsRepository\n ->getReportResultsQueryForRetention($team, $retentionDate);\n\n return $this->deleteReportResultsByQuery($query, [\n 'team_id' => $team->getId(),\n 'retention_date' => $retentionDate->toDateTimeString(),\n ]);\n }\n\n /**\n * Delete ALL report results for a specific automated report\n *\n * @param string $uuid\n *\n * @return int\n */\n public function deleteReportResults(string $uuid): int\n {\n $report = $this->getReport($uuid);\n\n $query = $this->automatedReportsRepository->getResultsByReportQuery($report);\n\n return $this->deleteReportResultsByQuery($query, [\n 'report_uuid' => $uuid,\n 'report_id' => $report->getId(),\n ]);\n }\n\n public function deleteReportResult(AutomatedReportResult $result): void\n {\n $this->deleteS3Files($result);\n\n $result->delete();\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getTeamReports(Team $team): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getReportsByTeam($team);\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getReportResults(AutomatedReport $report): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getResultsByReport($report);\n }\n\n public function deleteAllReportResults(AutomatedReport $report): void\n {\n $results = $this->getReportResults($report);\n\n /** @var AutomatedReportResult $result */\n foreach ($results as $result) {\n Log::info('Deleting result', [\n 'report' => $report->getId(),\n 'result' => $result->getId(),\n ]);\n\n $this->deleteReportResult($result);\n }\n }\n\n public function deleteAllData(Team $team): void\n {\n Log::info('Deleting automated report and results for team', [\n 'team' => $team->getId(),\n ]);\n\n $reports = $this->getTeamReports($team);\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n Log::info('Deleting report', [\n 'team' => $team->getId(),\n 'report' => $report->getId(),\n ]);\n\n $this->deleteAllReportResults($report);\n\n $report->delete();\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Carbon\\CarbonImmutable;\nuse Carbon\\CarbonInterface;\nuse Carbon\\Exceptions\\InvalidFormatException;\nuse DateTime;\nuse DateTimeInterface;\nuse DateTimeZone;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\InputTypeEnum;\nuse Jiminny\\Component\\AskAnything\\AskAnythingPromptService;\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Contracts\\Repositories\\PlaybookCategoryRepository;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\ModelNotFoundException;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Partner;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Repositories\\GroupRepository;\nuse Jiminny\\Repositories\\SearchRepository;\nuse Jiminny\\Repositories\\StageRepository;\nuse Throwable;\n\nclass AutomatedReportsService\n{\n public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';\n public const string TYPE_ASK_JIMINNY = 'ask_jiminny';\n\n /**\n * Standard report types (used by kiosk for existing automated reports).\n */\n // @TODO this will add filter, however if we need to control feature by FF we need conditional logic\n public const array TYPES = [\n ['id' => 'exec_summary', 'name' => 'Exec Summary'],\n ['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],\n ['id' => 'product_feedback', 'name' => 'Product Feedback'],\n ['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],\n// ['id' => 'questions', 'name' => 'Questions'],\n// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],\n ];\n\n public const array ALL_TYPES = [\n ...self::TYPES,\n ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],\n ];\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\n /**\n * Frequencies for standard (non-Ask Jiminny) reports.\n */\n public const array FREQUENCIES = [\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n\n /**\n * Frequencies for Ask Jiminny reports.\n */\n public const array ASK_JIMINNY_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ];\n\n public const string MEDIA_TYPE_PDF = 'pdf';\n public const string MEDIA_TYPE_PODCAST = 'podcast';\n public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];\n public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];\n public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];\n public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];\n\n public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];\n public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];\n public const int SENT_REPORT_AT_HOURS = 5;\n public const string PDF_KEY = 'pdf';\n public const string AUDIO_KEY = 'audio';\n\n private const array ALL_FREQUENCIES = [\n ['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],\n ['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],\n ['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],\n ['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],\n ['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],\n ];\n private const string S3_DIR = 'reports';\n private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];\n private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];\n\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly GroupRepository $groupRepository,\n private readonly UserRepository $userRepository,\n private readonly StageRepository $stageRepository,\n private readonly DealStagesService $dealStagesService,\n private readonly RecipientsService $recipientsService,\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly Webhook $webhookService,\n private readonly BusDispatcher $dispatcher,\n private readonly ActivityTypeService $activityTypeService,\n private readonly PlaybookCategoryRepository $playbookCategoryRepository,\n private readonly AskAnythingPromptService $askAnythingPromptService,\n private readonly SearchRepository $activitySearchRepository,\n private readonly AskAnythingRepository $askAnythingRepository,\n ) {\n }\n\n public static function getTypes(): array\n {\n $types = self::TYPES;\n\n return array_map(static function ($type) {\n return $type['id'];\n }, $types);\n }\n\n public static function getCallTypes(): array\n {\n return array_map(static function ($callType) {\n return $callType['id'];\n }, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);\n }\n\n public static function getFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::FREQUENCIES);\n }\n\n // front-facing structure\n public function getReportEnabledFieldData(bool $value = false): array\n {\n return [\n 'id' => 'report_enabled',\n 'label' => '',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'value' => $value,\n ];\n }\n\n // Organizations = Teams\n public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array\n {\n $options = $this->getTeams(partner: $partner);\n\n if ($shortVersion) {\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'organization',\n 'label' => 'Organization',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value,\n 'dependencies' => [\n 'teams',\n 'deal_stage_at_call',\n 'current_deal_stage',\n 'recipients',\n ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,\n ],\n 'dependsOn' => [],\n ];\n }\n\n // Teams = Groups\n public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array\n {\n if ($shortVersion) {\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'options' => $options,\n ];\n }\n\n return [\n 'id' => 'teams',\n 'label' => 'Team',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $options,\n 'value' => $value, // value should be an array of objects {id, name}\n 'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],\n 'dependsOn' => [],\n ];\n }\n\n public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array\n {\n $types = [];\n if ($team instanceof Team) {\n if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n $types = self::TYPES;\n }\n if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {\n $types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];\n }\n } else {\n $types = self::TYPES;\n }\n\n if ($shortVersion) {\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'options' => $types,\n ];\n }\n\n return [\n 'id' => 'report_type',\n 'label' => 'Report Type',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $types,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getFrequencyFieldData(?string $value = null): array\n {\n return [\n 'id' => 'frequency',\n 'label' => 'Frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::FREQUENCIES,\n 'value' => $value,\n 'dependencies' => ['period'],\n 'dependsOn' => [],\n ];\n }\n\n public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array\n {\n return [\n 'id' => 'period',\n 'label' => 'Select one-off period',\n 'inputType' => InputTypeEnum::DATE_RANGE,\n 'required' => true,\n 'placeholder' => 'Select',\n 'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],\n 'queryParams' => [\n 'startDate' => 'start_date_period',\n 'endDate' => 'end_date_period',\n ],\n 'dependencies' => [],\n 'dependsOn' => ['frequency'],\n ];\n }\n\n public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array\n {\n return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);\n }\n\n public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);\n }\n\n public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array\n {\n return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);\n }\n\n public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'deal_value',\n 'label' => 'Deal Value',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_deal_value',\n 'max' => 'max_deal_value',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array\n {\n $value = [];\n $conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;\n $dialerOn && $value[] = self::CALL_TYPE_DIALER;\n\n return [\n 'id' => 'call_type',\n 'label' => 'Call Type',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => [\n self::CALL_TYPE_CONFERENCE,\n self::CALL_TYPE_DIALER,\n ],\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getMediaTypeFieldData(?AutomatedReport $report = null): array\n {\n $value = [];\n\n if ($report) {\n $value = $this->transformMediaTypes($report);\n }\n\n return [\n 'id' => 'media_types',\n 'label' => 'Export as',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'required' => true,\n 'options' => self::MEDIA_TYPE_OBJECTS,\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array\n {\n return [\n 'id' => 'call_duration',\n 'label' => 'Call Duration',\n 'inputType' => InputTypeEnum::INTEGER_RANGE,\n 'required' => false,\n 'value' => ['min' => $valueMin, 'max' => $valueMax],\n 'queryParams' => [\n 'min' => 'min_call_duration',\n 'max' => 'max_call_duration',\n ],\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getRecipientsFieldData(?Team $team = null, array $value = []): array\n {\n return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);\n }\n\n public function getJiminnyRecipientsFieldData(array $value = []): array\n {\n return $this->recipientsService->getJiminnyRecipientsFieldData($value);\n }\n\n public function getAdditionalPromptInputFieldData(?string $value = null): array\n {\n return [\n 'id' => 'additional_prompt_input',\n 'label' => 'Special requirements',\n 'inputType' => InputTypeEnum::TEXTAREA,\n 'required' => false,\n 'placeholder' => 'What should be the focus of the report?',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n public function getCustomReportNameFieldData(?string $value = null): array\n {\n return [\n 'id' => 'custom_name',\n 'label' => 'Custom report name',\n 'inputType' => InputTypeEnum::TEXT,\n 'required' => false,\n 'placeholder' => 'Enter custom name',\n 'value' => $value,\n 'dependencies' => [],\n 'dependsOn' => [],\n ];\n }\n\n // data providers\n public function getTeams(?Partner $partner = null): array\n {\n $teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);\n\n $teamData = [];\n foreach ($teams as $team) {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n continue;\n }\n\n $teamData[] = $this->transformTeam($team);\n }\n\n return $teamData;\n }\n\n public function getTeamGroups(string $teamUuid): array\n {\n $data = [];\n $team = $this->getTeam($teamUuid);\n\n if ($team !== null) {\n $groups = $team->groups()->get();\n\n foreach ($groups as $group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array\n {\n $data = [];\n $teams = $this->getTeams(partner: $partner);\n\n foreach ($teams as $team) {\n if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {\n continue;\n }\n\n $data[] = [\n 'label' => $team['name'],\n 'groups' => $this->getTeamGroups($team['id']),\n ];\n }\n\n return $data;\n }\n\n public function getTeam(string $teamUuid): ?Team\n {\n return $this->teamRepository->idOrUuid($teamUuid);\n }\n\n public function getTeamById(int $teamId): ?Team\n {\n return $this->teamRepository->find($teamId);\n }\n\n public function getGroupsUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportGroups = $report->getGroups();\n foreach ($reportGroups as $groupId) {\n if ($group = $this->groupRepository->find($groupId)) {\n $uuids[] = $group->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getPlaybookCategoriesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $playbookCategories = $report->getPlaybookCategories();\n foreach ($playbookCategories as $id) {\n if ($category = $this->playbookCategoryRepository->find($id)) {\n $uuids[] = $category->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getDealAtCallStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getDealAtCallStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getCurrentDealStagesUuids(AutomatedReport $report): array\n {\n $uuids = [];\n $reportStages = $report->getCurrentDealStages();\n foreach ($reportStages as $id) {\n if ($stage = $this->stageRepository->find($id)) {\n $uuids[] = $stage->getUuid();\n }\n }\n\n return $uuids;\n }\n\n public function getUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getRecipients());\n }\n\n public function getJiminnyUsersUuids(AutomatedReport $report): array\n {\n return $this->extractUserUuids($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function extractUserUuids(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => $user->getUuid())\n ->values()\n ->all();\n }\n\n // get mail data\n public function getRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getRecipients());\n }\n\n /**\n * @return array<UserContract>\n */\n public function getRecipientUserObjects(AutomatedReport $report): array\n {\n $userIds = $report->getRecipients()['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->values()\n ->all();\n }\n\n private function getJiminnyRecipientUsers(AutomatedReport $report): array\n {\n return $this->buildRecipientUsers($report->getJiminnyRecipients());\n }\n\n /**\n * @param array<string, mixed> $recipients\n */\n private function buildRecipientUsers(array $recipients): array\n {\n $userIds = $recipients['users'] ?? [];\n\n return collect($userIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (UserContract $user) => [\n 'email' => $user->getEmailAddress(),\n 'name' => $user->getName(),\n 'timezone' => $user->getTimezone()->getName(),\n ])\n ->values()\n ->all();\n }\n\n public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array\n {\n if ($report->isAskJiminnyReport()) {\n $recipients = $this->resolveAskJiminnyRecipients($report);\n } else {\n $recipients = $this->getRecipientUsers($report);\n if ($includeJiminny) {\n $recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));\n }\n }\n\n $emails = [];\n\n return array_values(array_filter(\n $recipients,\n static function ($recipient) use (&$emails) {\n if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {\n return false;\n }\n $emails[] = $recipient['email'];\n\n return true;\n }\n ));\n }\n\n private function resolveAskJiminnyRecipients(AutomatedReport $report): array\n {\n $recipients = [];\n\n $creator = $report->getCreator();\n if ($creator !== null) {\n $recipients[] = [\n 'email' => $creator->getEmailAddress(),\n 'name' => $creator->getName(),\n 'timezone' => $creator->getTimezone()->getName(),\n ];\n }\n\n return array_merge(\n $recipients,\n $this->buildRecipientUsers($report->getRecipients()),\n $this->getGroupRecipientUsers($report),\n );\n }\n\n private function getGroupRecipientUsers(AutomatedReport $report): array\n {\n $users = [];\n foreach ($report->getGroups() as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group === null) {\n continue;\n }\n foreach ($group->getMembers() as $member) {\n $users[] = [\n 'email' => $member->getEmailAddress(),\n 'name' => $member->getName(),\n 'timezone' => $member->getTimezone()->getName(),\n ];\n }\n }\n\n return $users;\n }\n\n public function getReportTypeName(AutomatedReportResult $report): string\n {\n $type = $report->getReport()->getType();\n\n $getType = $this->transformReportType($type);\n\n return $getType['name'];\n }\n\n public function getReportPeriodName(AutomatedReportResult $report): string\n {\n $from = $report->getFromDate();\n $to = $report->getToDate();\n $frequency = $report->getReport()->getFrequency();\n\n if ($from === null || $to === null) {\n if (! $report->getReport()->isAskJiminnyReport()) {\n $invalidPeriod = $from === null ? 'from' : 'to';\n\n throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);\n }\n\n $timezone = $report->getReport()->getCreator()?->getTimezone();\n $period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);\n $from = $period['fromDate'];\n $to = $period['toDate'];\n }\n\n return $this->formatReportPeriodName($frequency, $from, $to);\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 getReportTeamsName(AutomatedReportResult $report): string\n {\n $groups = $report->getGroups();\n\n if (empty($groups)) {\n return 'All';\n }\n\n // Get group names from repository\n $groupNames = [];\n foreach ($groups as $groupId) {\n $group = $this->groupRepository->find($groupId);\n if ($group) {\n $groupNames[] = $group->getName();\n }\n }\n\n if (count($groupNames) === 1) {\n // Single team format\n $teamsName = $groupNames[0];\n } else {\n // Multiple teams format\n $teamsName = implode(', ', $groupNames);\n }\n\n return $teamsName;\n }\n\n public function getReportFileName(AutomatedReportResult $report): string\n {\n $customName = $report->getReport()->getCustomName();\n $periodName = $this->getReportPeriodName($report);\n $filenameSuffix = $this->getFilenameSuffix($report);\n\n if ($customName) {\n if ($filenameSuffix) {\n $customName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$customName} - {$periodName}\");\n }\n\n $baseName = $this->getReportTypeName($report);\n\n if ($filenameSuffix) {\n $baseName .= \" {$filenameSuffix}\";\n }\n\n return $this->sanitizeFileName(\"{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}\");\n }\n\n public function getReportFileNameWithExtension(AutomatedReportResult $result): string\n {\n $extension = $this->getMediaTypeMetadata($result)['extension'];\n\n return $this->getReportFileName($result) . '.' . $extension;\n }\n\n public function sanitizeFileName(string $fileName): string\n {\n return str_replace(['/', '\\\\'], '-', $fileName);\n }\n\n public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool\n {\n $recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);\n if (in_array($user->getId(), $recipientIds, true)) {\n return true;\n }\n\n if ($report->isAskJiminnyReport()) {\n $groupId = $user->getGroupId();\n if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {\n return true;\n }\n }\n\n return false;\n }\n\n public function transformReportResults(Collection $automatedReportResults): array\n {\n $data = [];\n foreach ($automatedReportResults as $automatedReportResult) {\n /** @var AutomatedReportResult $automatedReportResult */\n\n $report = $automatedReportResult->getReport();\n\n $createdBy = $report->getCreator();\n $creator = [\n 'id' => $createdBy?->getUuid(),\n 'name' => $createdBy?->getName(),\n 'email' => $createdBy?->getEmailAddress(),\n 'photoUrl' => $createdBy?->getPhotoUrl(),\n ];\n\n $data[] = [\n 'id' => $automatedReportResult->getUuid(),\n 'name' => $automatedReportResult->getName(),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n 'recipients' => $this->buildRecipients($report),\n 'report_type' => $this->transformReportType($report->getType()),\n 'media_type' => $automatedReportResult->getMediaType(),\n 'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),\n 'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),\n 'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),\n 'creator' => $creator,\n ];\n }\n\n return $data;\n }\n\n private function buildRecipients(AutomatedReport $report): array\n {\n $creatorUuid = $report->getCreator()?->getUuid();\n\n $recipients = array_values(array_filter(\n $this->transformRecipients($report->getRecipients()),\n static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,\n ));\n\n if (! $report->isAskJiminnyReport()) {\n return $recipients;\n }\n\n return [\n ...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),\n ...$recipients,\n ];\n }\n\n public function hasCallTypeConference(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);\n }\n\n public function hasCallTypeDialer(AutomatedReport $report): bool\n {\n return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);\n }\n\n // transformers\n private function transformTeam(Team $team): array\n {\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n return [];\n }\n\n return [\n 'id' => $team->getUuid(),\n 'name' => $team->getName(),\n ];\n }\n\n private function transformReportFullView(AutomatedReport $report): array\n {\n $base = $this->transformReportBase($report);\n\n return $report->getType() === self::TYPE_ASK_JIMINNY\n ? $base + $this->transformAskJiminnyFields($report)\n : $base + $this->transformStandardReportFields($report);\n }\n\n private function transformReportBase(AutomatedReport $report): array\n {\n return [\n 'id' => $report->getUuid(),\n 'organization' => $this->transformOrganization(team: $report->getTeam()),\n 'report_type' => $this->transformReportType($report->getType()),\n 'frequency' => $this->transformFrequency($report->getFrequency()),\n ];\n }\n\n private function transformStandardReportFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n\n return [\n 'report_enabled' => $report->getStatus(),\n 'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),\n 'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),\n 'deal_value_min' => $report->getDealValueMin(),\n 'deal_value_max' => $report->getDealValueMax(),\n 'call_types' => $this->transformCallType($report->getCallTypes()),\n 'media_types' => $this->transformMediaTypes($report),\n 'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),\n 'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),\n 'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),\n 'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),\n 'recipients' => $this->transformRecipients($report->getRecipients()),\n 'created_by' => $this->transformCreator($report->getCreator()),\n 'additional_prompt_input' => $report->getAdditionalPromptInput(),\n 'custom_name' => $report->getCustomName(),\n 'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),\n 'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),\n 'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),\n ];\n }\n\n private function transformAskJiminnyFields(AutomatedReport $report): array\n {\n $team = $report->getTeam();\n $creatorId = $report->getAttribute('created_by');\n $explicitUserIds = array_values(array_filter(\n $report->getRecipients()['users'] ?? [],\n static fn ($id) => $id !== $creatorId\n ));\n\n return [\n 'report_name' => $report->getCustomName(),\n 'enabled' => $report->getStatus(),\n 'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),\n 'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),\n 'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),\n 'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),\n 'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),\n ];\n }\n\n private function transformOrganization(?Team $team): array\n {\n return [\n 'id' => $team?->getUuid(),\n 'name' => $team?->getName(),\n ];\n }\n\n private function transformReportType(string $type): array\n {\n foreach (self::ALL_TYPES as $typeItem) {\n if ($typeItem['id'] === $type) {\n return $typeItem;\n }\n }\n\n return [];\n }\n\n private function transformCallType(array $types): array\n {\n $result = [];\n $callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];\n\n foreach ($types as $type) {\n foreach ($callTypes as $callTypeItem) {\n if ($callTypeItem['id'] === $type) {\n $result[] = $callTypeItem;\n\n break;\n }\n }\n }\n\n return $result;\n }\n\n private function transformMediaTypes(AutomatedReport $report): array\n {\n $values = [];\n\n foreach ($report->getMediaTypes() as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n continue;\n }\n\n $values[] = match ($mediaType) {\n self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,\n self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,\n };\n }\n\n return $values;\n }\n\n private function transformFrequency(string $frequency): array\n {\n foreach (self::ALL_FREQUENCIES as $frequencyItem) {\n if ($frequencyItem['id'] === $frequency) {\n return $frequencyItem;\n }\n }\n\n return [];\n }\n\n public function transformDurationToMinutes(?int $duration): ?int\n {\n if (! $duration) {\n return null;\n }\n\n return (int) ($duration / 60);\n }\n\n private function transformGroups(?Team $team, array $groupsIds): array\n {\n if (empty($groupsIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($groupsIds as $groupId) {\n $group = $team->groups()->where('id', $groupId)->first();\n\n if ($group) {\n $data[] = [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n 'photoUrl' => $group->getPhotoUrl(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformStages(?Team $team, array $stagesIds): array\n {\n if (empty($stagesIds) || ! $team) {\n return [];\n }\n\n $data = [];\n foreach ($stagesIds as $stageId) {\n $stage = $team->stages()->where('id', $stageId)->first();\n\n if ($stage) {\n $data[] = [\n 'id' => $stage->getUuid(),\n 'name' => $stage->getName(),\n ];\n }\n }\n\n return $data;\n }\n\n private function transformRecipients(array $recipients): array\n {\n $users = [];\n foreach ($recipients['users'] ?? [] as $userId) {\n $users[] = $this->transformUser($userId);\n }\n\n return $users;\n }\n\n private function transformCreator(?User $user): ?array\n {\n if ($user === null) {\n return null;\n }\n\n return $this->transformUser($user->getId());\n }\n\n private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array\n {\n if ($prompt === null) {\n return null;\n }\n\n return [\n 'id' => $prompt->getUuid(),\n 'name' => $prompt->getTitle(),\n ];\n }\n\n private function transformSafeSearch(?Search $search): ?array\n {\n if ($search === null) {\n return null;\n }\n\n return [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ];\n }\n\n private function transformUser(int $userId): array\n {\n /* @var ?User $user */\n $user = $this->userRepository->find($userId);\n\n return [\n 'id' => $user?->getUuid(),\n 'name' => $user?->getName(),\n 'email' => $user?->getEmailAddress(),\n 'photoUrl' => $user?->getPhotoUrl(),\n ];\n }\n\n public function create(array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $validatedData['created_by'] = auth()->id();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function update(string $uuid, array $data): array\n {\n $validatedData = $this->validateAndTransformData($data);\n $report = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $report) {\n throw new InvalidArgumentException('Report not found');\n }\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Create an Ask Jiminny report.\n */\n public function createAskJiminnyReport(array $data, User $creator): array\n {\n $validatedData = $this->validateAskJiminnyReportData($data, $creator);\n $validatedData['created_by'] = $creator->getId();\n\n $automatedReport = $this->automatedReportsRepository->create($validatedData);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n /**\n * Update an Ask Jiminny report.\n */\n public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array\n {\n if (! $report->isAskJiminnyReport()) {\n throw new InvalidArgumentException('Report is not an Ask Jiminny report');\n }\n\n $validatedData = $this->validateAskJiminnyReportData($data, $user);\n\n $oldCustomName = $report->getCustomName();\n\n $automatedReport = $this->automatedReportsRepository->update($report, $validatedData);\n\n if ($oldCustomName !== $automatedReport->getCustomName()) {\n $this->updateResultNames($automatedReport);\n }\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array\n {\n if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {\n throw new InvalidArgumentException(\n 'This report is missing a saved search or prompt. ' .\n 'Edit the report to complete the setup before enabling it.'\n );\n }\n\n $this->automatedReportsRepository->update($report, ['status' => $status]);\n\n return $this->transformReportFullView($report->fresh());\n }\n\n /**\n * Validate and transform data for Ask Jiminny reports.\n */\n private function validateAskJiminnyReportData(array $data, User $user): array\n {\n // Validate name\n $name = trim($data['report_name'] ?? '');\n if (empty($name)) {\n throw new InvalidArgumentException('Report name is required');\n }\n if (mb_strlen($name) > 50) {\n throw new InvalidArgumentException('Report name must be 50 characters or less');\n }\n\n // Validate frequency (only daily, weekly, monthly for Ask Jiminny)\n $frequency = $data['frequency'] ?? null;\n $askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];\n if (! in_array($frequency, $askJiminnyFrequencies, true)) {\n throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');\n }\n\n // Validate expiration date\n $expiresAt = $data['expires_on'] ?? null;\n if (empty($expiresAt)) {\n throw new InvalidArgumentException('Expiration date is required');\n }\n\n try {\n $expiresAtDate = Carbon::parse($expiresAt);\n } catch (InvalidFormatException $e) {\n throw new InvalidArgumentException('Expiration date format is invalid');\n }\n $maxExpiration = Carbon::now()->addYear()->endOfDay();\n if ($expiresAtDate->gt($maxExpiration)) {\n throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');\n }\n if ($expiresAtDate->isPast()) {\n throw new InvalidArgumentException('Expiration date cannot be in the past');\n }\n\n // Validate saved search\n $activitySearchId = $data['saved_search'] ?? null;\n if (empty($activitySearchId)) {\n throw new InvalidArgumentException('Saved search is required');\n }\n $savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);\n if (! $savedSearch) {\n throw new InvalidArgumentException('Saved search not found or does not belong to you');\n }\n\n // Validate saved prompt\n $askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;\n if (empty($askAnythingPromptId)) {\n throw new InvalidArgumentException('Ask Jiminny prompt is required');\n }\n $prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);\n if (! $prompt) {\n throw new InvalidArgumentException('Ask Jiminny prompt not found');\n }\n\n // Validate status\n $status = $data['enabled'] ?? false;\n\n $recipientUserIds = [$user->getId()];\n\n if (! empty($data['share_users'])) {\n $sharedUserIds = $this->validateAndGetUserIdsByTeam(\n $user->team,\n (array) $data['share_users']\n );\n $recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);\n }\n\n $sharedGroupIds = [];\n if (! empty($data['share_teams'])) {\n $sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);\n }\n\n $recipientUserIds = array_values(array_unique($recipientUserIds));\n\n return [\n 'team_id' => $user->getTeamId(),\n 'type' => self::TYPE_ASK_JIMINNY,\n 'status' => (bool) $status,\n 'frequency' => $frequency,\n 'custom_name' => $name,\n 'activity_search_id' => $savedSearch->getId(),\n 'ask_anything_prompt_id' => $prompt->getId(),\n 'expires_at' => $expiresAtDate->toDateString(),\n 'media_types' => [self::MEDIA_TYPE_PDF],\n 'call_types' => [],\n 'recipients' => ['users' => $recipientUserIds],\n 'groups' => $sharedGroupIds,\n ];\n }\n\n public static function getAskJiminnyFrequencies(): array\n {\n return array_map(static function ($frequency) {\n return $frequency['id'];\n }, self::ASK_JIMINNY_FREQUENCIES);\n }\n\n public function getAskJiminnyReportFilters(User $user): array\n {\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n return [\n [\n 'id' => 'prompt',\n 'label' => 'Prompt',\n 'options' => $prompts,\n ],\n [\n 'id' => 'saved_search',\n 'label' => 'Saved Search',\n 'options' => $savedSearches,\n ],\n ];\n }\n\n public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array\n {\n $team = $user->getTeam();\n $userTimezone = $user->getTimezone();\n\n $savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)\n ->map(fn (Search $search) => [\n 'id' => $search->getUuid(),\n 'name' => $search->getName(),\n ])\n ->values()->all();\n\n $prompts = collect(\n $this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)\n )->map(fn (AskAnythingPromptDto $prompt) => [\n 'id' => $prompt->id,\n 'name' => $prompt->title,\n ])->values()->all();\n\n $teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [\n 'id' => $group->getUuid(),\n 'name' => $group->getName(),\n ])->values()->all();\n\n $shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];\n\n $sharedTeamsValue = [];\n $sharedUsersValue = [];\n if ($report) {\n $sharedTeamsValue = $this->transformGroups($team, $report->getGroups());\n\n $recipientUserIds = $report->getRecipients()['users'] ?? [];\n $creatorId = $report->getAttribute('created_by');\n $sharedUserIds = array_values(array_filter(\n $recipientUserIds,\n static fn ($id) => $id !== $creatorId\n ));\n $sharedUsersValue = collect($sharedUserIds)\n ->map(fn ($id) => $this->userRepository->find((int) $id))\n ->filter()\n ->map(fn (User $u) => [\n 'id' => $u->getUuid(),\n 'name' => $u->getName(),\n ])\n ->values()\n ->all();\n }\n\n return [\n 'fields' => [\n [\n 'id' => 'enabled',\n 'inputType' => InputTypeEnum::TOGGLE,\n 'label' => '',\n 'value' => $report?->getStatus() ?? false,\n ],\n [\n 'id' => 'report_name',\n 'inputType' => InputTypeEnum::TEXT,\n 'label' => 'Name',\n 'placeholder' => 'Enter name',\n 'required' => true,\n 'validation' => ['maxLength' => 50],\n 'value' => $report?->getCustomName() ?? '',\n ],\n [\n 'id' => 'frequency',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Frequency',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => self::ASK_JIMINNY_FREQUENCIES,\n 'value' => $report ? $this->transformFrequency($report->getFrequency()) : null,\n ],\n [\n 'id' => 'expires_on',\n 'inputType' => InputTypeEnum::DATE,\n 'label' => 'Expires on',\n 'required' => true,\n 'placeholder' => 'Select',\n 'validation' => [\n 'minDate' => now($userTimezone)->toDateString(),\n 'maxDate' => now($userTimezone)->addYear()->toDateString(),\n ],\n 'value' => $report?->getExpiresAt()?->toDateString(),\n ],\n [\n 'id' => 'share_teams',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team',\n 'required' => false,\n 'placeholder' => 'Select',\n 'options' => $teamGroups,\n 'value' => $sharedTeamsValue,\n ],\n [\n 'id' => 'share_users',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'label' => 'Team member',\n 'required' => false,\n 'placeholder' => 'Select',\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'options' => $shareUsers,\n 'value' => $sharedUsersValue,\n ],\n [\n 'id' => 'saved_search',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Saved search',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $savedSearches,\n 'value' => $report && $report->getSavedSearch() ? [\n 'id' => $report->getSavedSearch()->getUuid(),\n 'name' => $report->getSavedSearch()->getName(),\n ] : null,\n ],\n [\n 'id' => 'ask_jiminny_prompt',\n 'inputType' => InputTypeEnum::DROPDOWN,\n 'label' => 'Ask Jiminny prompt',\n 'required' => true,\n 'placeholder' => 'Select',\n 'options' => $prompts,\n 'value' => $report && $report->getAskAnythingPrompt() ? [\n 'id' => $report->getAskAnythingPrompt()->getUuid(),\n 'name' => $report->getAskAnythingPrompt()->getTitle(),\n ] : null,\n ],\n ],\n ];\n }\n\n private function updateResultNames(AutomatedReport $automatedReport): void\n {\n $results = $this->automatedReportsRepository->getResultsByReport($automatedReport);\n\n foreach ($results as $result) {\n $result->update(['name' => $this->getReportFileName($result)]);\n }\n }\n\n public function updateStatus(string $uuid, array $data): array\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $automatedReport->update([\n 'status' => $status,\n ]);\n\n $this->generateOneOffReport($automatedReport);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n private function generateOneOffReport(AutomatedReport $automatedReport): void\n {\n // the scheduler handles all the other frequency types\n if ($automatedReport->getStatus() === false || $automatedReport->getFrequency() !== self::FREQUENCY_ONE_OFF) {\n return;\n }\n\n $this->dispatcher->dispatch(new RequestGenerateReportJob($automatedReport->getUuid()));\n }\n\n public function getReport(string $uuid, ?Partner $partner = null): AutomatedReport\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n if ($partner !== null && ! $partner->isDefaultPartner() && $automatedReport->team->partner_id !== $partner->getId()) {\n throw new ModelNotFoundException('Report not found');\n }\n\n return $automatedReport;\n }\n\n public function get(string $uuid, ?Partner $partner = null): array\n {\n $automatedReport = $this->getReport($uuid, $partner);\n\n return $this->transformReportFullView($automatedReport);\n }\n\n public function list(string $sortColumn = 'created_at', string $sortDirection = 'desc', ?Partner $partner = null): array\n {\n $results = [];\n $collection = $this->automatedReportsRepository->getAllStandardReports($sortColumn, $sortDirection, $partner);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function listAskJiminnyReports(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): array {\n $results = [];\n $collection = $this->automatedReportsRepository->getAskJiminnyReportsByUser($user, $sortColumn, $sortDirection);\n\n /** @var AutomatedReport $report */\n foreach ($collection as $report) {\n $results[] = $this->transformReportFullView($report);\n }\n\n return ['data' => $results];\n }\n\n public function delete(string $uuid): void\n {\n $automatedReport = $this->automatedReportsRepository->findByUuid($uuid);\n\n if (! $automatedReport) {\n throw new ModelNotFoundException('Report not found');\n }\n\n $automatedReport->delete();\n }\n\n public function createReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n return $this->automatedReportsRepository->createResult(\n array_merge(\n [\n 'report_id' => $automatedReport->getId(),\n 'status' => AutomatedReportResult::STATUS_DEFAULT,\n ],\n $data\n )\n );\n }\n\n public function getOrCreateReportResult(AutomatedReport $automatedReport, array $data = []): AutomatedReportResult\n {\n $existing = $this->automatedReportsRepository->findLatestSameDayDefaultOrFailedResult($automatedReport);\n\n if ($existing !== null) {\n $existing->update(['status' => AutomatedReportResult::STATUS_DEFAULT]);\n\n return $existing;\n }\n\n return $this->createReportResult($automatedReport, $data);\n }\n\n public function getReportResult(string $resultUuid): AutomatedReportResult\n {\n $report = $this->automatedReportsRepository->findResultByUuid($resultUuid);\n\n if (! $report) {\n throw new ModelNotFoundException('Report Result not found');\n }\n\n return $report;\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return $this->automatedReportsRepository->findChildResult($result, $type);\n }\n\n // prophet API calls\n /**\n * @throws ApplicationException\n */\n public function getGenerateReportPayload(AutomatedReport $automatedReport, string $reportResultUuid): array\n {\n $period = $this->calculateFromAndToDate($automatedReport);\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n return [\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResultUuid,\n 'report_type' => $automatedReport->getType(),\n 'media_types' => $automatedReport->getMediaTypes(),\n 'from_date' => $fromDate->startOfDay()->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->endOfDay()->format(DateTimeInterface::RFC3339),\n 'group_ids' => $automatedReport->getGroups(),\n 'call_deal_stage' => $automatedReport->getDealAtCallStages(),\n 'current_deal_stage' => $automatedReport->getCurrentDealStages(),\n 'deal_min_value' => $automatedReport->getDealValueMin(),\n 'deal_max_value' => $automatedReport->getDealValueMax(),\n 'call_types' => $automatedReport->getCallTypes(),\n 'call_duration_min_seconds' => $automatedReport->getCallDurationMin(),\n 'call_duration_max_seconds' => $automatedReport->getCallDurationMax(),\n 'special_requirements' => $automatedReport->getAdditionalPromptInput(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->formatReportPeriodName(\n $automatedReport->getFrequency(),\n $fromDate,\n $toDate,\n ),\n 'playbook_categories' => $automatedReport->getPlaybookCategories(),\n 'custom_name' => $automatedReport->getCustomName(),\n ];\n }\n\n // $inputPayload - FE payload structure\n public function getActivitiesCountPayload(array $inputPayload): array\n {\n // Use validateAndTransformData to validate and normalize input\n $validatedData = $this->validateAndTransformData($inputPayload);\n $period = $this->calculateFromAndToDatePeriod(\n $validatedData['frequency'],\n Carbon::parse($validatedData['from']),\n Carbon::parse($validatedData['to']),\n );\n $fromDate = $period['fromDate'];\n $toDate = $period['toDate'];\n\n // Create payload similar to getGenerateReportPayload\n return [\n 'team_id' => $validatedData['team_id'],\n 'group_ids' => $validatedData['groups'] ?? [],\n 'report_type' => $validatedData['type'],\n 'from_date' => $fromDate->format(DateTimeInterface::RFC3339),\n 'to_date' => $toDate->format(DateTimeInterface::RFC3339),\n 'call_deal_stage' => $validatedData['deal_at_call_stages'] ?? [],\n 'current_deal_stage' => $validatedData['current_deal_stages'] ?? [],\n 'deal_min_value' => $validatedData['deal_value_min'] ?? null,\n 'deal_max_value' => $validatedData['deal_value_max'] ?? null,\n 'call_types' => $validatedData['call_types'],\n 'call_duration_min_seconds' => $validatedData['call_duration_min'] ?? null,\n 'call_duration_max_seconds' => $validatedData['call_duration_max'] ?? null,\n 'special_requirements' => $validatedData['additional_prompt_input'] ?? null,\n 'playbook_categories' => $validatedData['playbook_categories'] ?? [],\n 'request_id' => null,\n 'callback_url' => null,\n ];\n }\n\n public function shouldSendReport(array $users, ?CarbonInterface $generatedAt = null): bool\n {\n if (empty($users)) {\n return false;\n }\n\n $earliestTz = collect($users)\n ->mapWithKeys(function (array $user) {\n $tz = new DateTimeZone($user['timezone']);\n $nowUtc = new DateTime('now', new DateTimeZone('UTC'));\n $offset = $tz->getOffset($nowUtc);\n\n return [$user['timezone'] => $offset];\n })\n ->sortDesc()\n ->keys()\n ->first();\n\n $now = Carbon::now($earliestTz);\n $isScheduledTime = (int) $now->format('H') === self::SENT_REPORT_AT_HOURS;\n\n if ($isScheduledTime) {\n return true;\n }\n\n return $this->hasPassedScheduledTime($generatedAt, $earliestTz);\n }\n\n public function hasPassedScheduledTime(?CarbonInterface $generatedAt, string $timezone): bool\n {\n if ($generatedAt === null) {\n return false;\n }\n\n $now = Carbon::now($timezone);\n $scheduledTime = $now->copy()->setTime(self::SENT_REPORT_AT_HOURS, 0, 0);\n\n if ($now->hour < self::SENT_REPORT_AT_HOURS) {\n $scheduledTime = $scheduledTime->subDay();\n }\n\n $scheduledTimeUtc = $scheduledTime->copy()->utc();\n $generatedAtUtc = $generatedAt->copy()->utc();\n $nowUtc = $now->copy()->utc();\n\n return $generatedAtUtc->lt($scheduledTimeUtc) && $nowUtc->gt($scheduledTimeUtc);\n }\n\n public function calculateFromAndToDatePeriod(\n string $frequency,\n ?Carbon $fromDate = null,\n ?Carbon $toDate = null,\n DateTimeZone|string|null $timezone = 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($timezone);\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()->subWeek()->startOfWeek(CarbonInterface::MONDAY),\n 'toDate' => $now->copy()->subWeek()->endOfWeek(CarbonInterface::SUNDAY),\n ],\n self::FREQUENCY_MONTHLY => [\n 'fromDate' => $now->copy()->subMonthNoOverflow()->startOfMonth(),\n 'toDate' => $now->copy()->subMonthNoOverflow()->endOfMonth(),\n ],\n self::FREQUENCY_QUARTERLY => [\n 'fromDate' => $now->copy()->subQuarterNoOverflow()->startOfQuarter(),\n 'toDate' => $now->copy()->subQuarterNoOverflow()->endOfQuarter(),\n ],\n default => throw new InvalidArgumentException(\"Unsupported frequency: {$frequency}\"),\n };\n }\n\n private function calculateFromAndToDate(AutomatedReport $automatedReport): array\n {\n return $this->calculateFromAndToDatePeriod(\n $automatedReport->getFrequency(),\n $automatedReport->getFrom(),\n $automatedReport->getTo()\n );\n }\n\n public function getAskJiminnyGenerateReportPayload(\n AutomatedReport $automatedReport,\n AutomatedReportResult $reportResult,\n array $activityIds,\n ): array {\n return [\n 'user_question' => $automatedReport->getAskAnythingPrompt()?->getContent(),\n 'call_ids' => array_map('strval', $activityIds),\n 'team_id' => $automatedReport->getTeamId(),\n 'request_id' => $reportResult->getUuid(),\n 'callback_url' => $this->getCallbackUrl(),\n 'report_period' => $this->getReportPeriodName($reportResult),\n 'report_name' => $automatedReport->getCustomName(),\n ];\n }\n\n private function getCallbackUrl(): string\n {\n return $this->webhookService->route('jiminny.webhook.reports.ready');\n }\n\n /**\n * Validate and transform payload data for automated reports\n *\n * @param array $data\n *\n * @throws InvalidArgumentException\n *\n * @return array\n */\n private function validateAndTransformData(array $data): array\n {\n // Validate organization (team) and check feature\n $team = $this->validateOrganization($data['organization'] ?? null);\n\n $status = $this->validateReportStatus($data['report_enabled'] ?? null);\n $type = $this->validateReportType($data['report_type'] ?? null);\n $frequency = $this->validateFrequency($data['frequency'] ?? null);\n $additionalPromptInput = $this->validateAdditionalPromptInput(\n $data['additional_prompt_input'] ?? null\n );\n $customReportName = $this->validateCustomReportName($data['custom_name'] ?? null);\n\n // Prepare data for the database\n $reportData = [\n 'team_id' => $team->getId(),\n 'type' => $type,\n 'status' => $status,\n 'frequency' => $frequency,\n 'additional_prompt_input' => $additionalPromptInput,\n 'custom_name' => $customReportName,\n ];\n\n // Validate deal values\n $reportData = $this->validateDealValues($data, $reportData);\n\n // Validate date range\n $reportData = $this->validateDateRange($data, $reportData, $frequency);\n\n // Validate call durations\n $reportData = $this->validateCallDurations($data, $reportData);\n\n // Validate call types\n $reportData = $this->validateCallTypes($data, $reportData);\n\n // Validate media types\n $reportData = $this->validateMediaTypes($data, $reportData);\n\n // Validate groups\n if (isset($data['teams'])) {\n $reportData['groups'] = $this->validateAndGetGroupIds($team, $data['teams']);\n }\n\n // Validate deal stages\n $reportData = $this->validateDealStages($data, $reportData, $team, $type);\n\n // Validate playbook categories\n $reportData = $this->validatePlaybookCategories($data, $reportData, $team);\n\n // Validate recipients\n $reportData['recipients'] = [\n 'users' => $this->validateAndGetUserIdsByTeam($team, $data['recipients'] ?? []),\n ];\n\n if (isset($data['jiminny_recipients'])) {\n // Validate Jiminny recipients\n $reportData['jiminny_recipients'] = [\n 'users' => $this->validateAndGetJiminnyUserIds((array) $data['jiminny_recipients']),\n ];\n }\n\n return $reportData;\n }\n\n private function validateDealValues(array $data, array $reportData): array\n {\n if (isset($data['min_deal_value'])) {\n $reportData['deal_value_min'] = (int) $data['min_deal_value'];\n\n if ($reportData['deal_value_min'] > 4294967295 || $reportData['deal_value_min'] < 0) {\n throw new InvalidArgumentException('Min deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_deal_value'])) {\n $reportData['deal_value_max'] = (int) $data['max_deal_value'];\n\n if ($reportData['deal_value_max'] > 4294967295 || $reportData['deal_value_max'] < 0) {\n throw new InvalidArgumentException('Max deal value should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['min_deal_value'], $data['max_deal_value'])\n && $data['min_deal_value'] > $data['max_deal_value']\n ) {\n throw new InvalidArgumentException('Min deal value cannot be greater than max deal value');\n }\n\n return $reportData;\n }\n\n private function validateDateRange(array $data, array $reportData, string $frequency): array\n {\n // Set date range only for one_off frequency\n if ($frequency === 'one_off') {\n if (isset($data['start_date_period'])) {\n $reportData['from'] = $this->parseDate($data['start_date_period']);\n }\n\n if (isset($data['end_date_period'])) {\n $reportData['to'] = $this->parseDate($data['end_date_period']);\n }\n\n if (empty($reportData['from']) || empty($reportData['to'])) {\n throw new InvalidArgumentException(\n 'Start date and end date are required for one_off frequency'\n );\n }\n } else {\n $reportData['from'] = null;\n $reportData['to'] = null;\n }\n\n return $reportData;\n }\n\n private function validateCallDurations(array $data, array $reportData): array\n {\n // Convert call durations from minutes to seconds\n if (isset($data['min_call_duration'])) {\n $reportData['call_duration_min'] = (int) $data['min_call_duration'] * 60;\n\n if ($reportData['call_duration_min'] > 4294967295 || $reportData['call_duration_min'] < 0) {\n throw new InvalidArgumentException('Min call duration should be between 0 and 4294967295');\n }\n }\n\n if (isset($data['max_call_duration'])) {\n $reportData['call_duration_max'] = (int) $data['max_call_duration'] * 60;\n\n if ($reportData['call_duration_max'] > 4294967295 || $reportData['call_duration_max'] < 0) {\n throw new InvalidArgumentException('Max call duration should be between 0 and 4294967295');\n }\n }\n\n return $reportData;\n }\n\n private function validateCallTypes(array $data, array $reportData): array\n {\n // Set call types\n $reportData['call_types'] = $data['call_type'] ?? [];\n if (empty($reportData['call_types'])) {\n $reportData['call_types'] = self::getCallTypes();\n }\n\n foreach ($reportData['call_types'] as $callType) {\n if (! in_array($callType, self::getCallTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Call type %s is invalid', $callType));\n }\n }\n\n return $reportData;\n }\n\n private function validateMediaTypes(array $data, array $reportData): array\n {\n // Set media types from input data\n $reportData['media_types'] = $data['media_types'] ?? [];\n\n if (empty($reportData['media_types'])) {\n throw new InvalidArgumentException('Media types are required');\n }\n\n foreach ($reportData['media_types'] as $mediaType) {\n if (! in_array($mediaType, self::MEDIA_TYPES, true)) {\n throw new InvalidArgumentException(sprintf('Media type %s is invalid', $mediaType));\n }\n }\n\n return $reportData;\n }\n\n private function validateDealStages(array $data, array $reportData, Team $team, string $reportType): array\n {\n // Validate and set deal stages\n if (isset($data['deal_stage_at_call'])) {\n $reportData['deal_at_call_stages'] =\n $this->validateAndGetDealStageIds($team, $data['deal_stage_at_call'], 'Deal stage at call');\n }\n\n if (isset($data['current_deal_stage'])) {\n $reportData['current_deal_stages'] =\n $this->validateAndGetDealStageIds($team, $data['current_deal_stage'], 'Current deal stage');\n }\n\n // Ensure current_deal_stage is not provided for loss_analysis report type\n if ($reportType === self::TYPE_LOSS_ANALYSIS && ! empty($data['current_deal_stage'])) {\n throw new InvalidArgumentException('Current deal stage is not applicable for Loss Analysis reports');\n }\n\n return $reportData;\n }\n\n // transform uuid to id\n private function validatePlaybookCategories(array $data, array $reportData, Team $team): array\n {\n $key = 'playbook_categories';\n\n if (isset($data[$key])) {\n $payloadIds = $data[$key];\n $ids = [];\n\n foreach ($payloadIds as $uuid) {\n $uuid = (string) $uuid;\n\n try {\n $playbookCategory = $this->playbookCategoryRepository->findByUuid($uuid);\n } catch (Throwable $throwable) {\n Log::error(__METHOD__ . ' ' . $throwable->getMessage());\n\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory) {\n throw new InvalidArgumentException(sprintf('Playbook category %s not found', $uuid));\n }\n\n if (! $playbookCategory->hasPlaybook()) {\n throw new InvalidArgumentException(sprintf('Playbook category %s has no playbook', $uuid));\n }\n\n if ($playbookCategory->getPlaybook()->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Playbook category %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $ids[] = $playbookCategory->getId();\n }\n\n $reportData[$key] = $ids;\n }\n\n return $reportData;\n }\n\n private function validateReportStatus($status): bool\n {\n if (! in_array($status, [true, false], true)) {\n throw new InvalidArgumentException('Report status is invalid');\n }\n\n return $status;\n }\n\n private function validateReportType($type): string\n {\n if (! in_array($type, self::getTypes(), true)) {\n throw new InvalidArgumentException(sprintf('Report type is invalid: %s', $type));\n }\n\n return $type;\n }\n\n private function validateFrequency($frequency): string\n {\n if (! in_array($frequency, self::getFrequencies(), true)) {\n throw new InvalidArgumentException('Frequency is invalid');\n }\n\n return $frequency;\n }\n\n private function validateAdditionalPromptInput(?string $additionalPromptInput): ?string\n {\n if ($additionalPromptInput && strlen($additionalPromptInput) > 5000) {\n throw new InvalidArgumentException('Additional Prompt Input should be less than 5000 characters');\n }\n\n return $additionalPromptInput;\n }\n\n private function validateCustomReportName(?string $customReportName): ?string\n {\n if ($customReportName === null || $customReportName === '') {\n return null;\n }\n\n if (strlen($customReportName) > 70) {\n throw new InvalidArgumentException('Custom report name should be less than 70 characters');\n }\n\n return $customReportName;\n }\n\n private function validateOrganization(?string $organizationUuid): Team\n {\n if (! $organizationUuid) {\n throw new InvalidArgumentException('Organization is required');\n }\n\n $team = $this->teamRepository->idOrUuid($organizationUuid);\n\n if (! $team) {\n throw new InvalidArgumentException('Organization not found');\n }\n\n if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {\n throw new InvalidArgumentException('Organization does not have the Automated Reports feature');\n }\n\n return $team;\n }\n\n private function validateAndGetGroupIds(Team $team, array $teamUuids): array\n {\n $groupIds = [];\n\n foreach ($teamUuids as $uuid) {\n $group = $this->groupRepository->findByUuid($uuid);\n\n if ($group === null || $group->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Group %s not found for team %s', $uuid, $team->getUuid())\n );\n }\n\n $groupIds[] = $group->getId();\n\n }\n\n return $groupIds;\n }\n\n private function validateAndGetDealStageIds(Team $team, array $stageUuids, string $propertyLabel): array\n {\n $stageIds = [];\n\n foreach ($stageUuids as $uuid) {\n $stage = $this->stageRepository->findByUuid($uuid);\n\n if ($stage === null || $stage->getTeamId() !== $team->getId()) {\n throw new InvalidArgumentException(\n sprintf('Stage %s not found for team %s for %s', $uuid, $team->getUuid(), $propertyLabel)\n );\n }\n\n $stageIds[] = $stage->getId();\n }\n\n return $stageIds;\n }\n\n private function validateAndGetUserIds(array $userUuids, callable $teamCheck): array\n {\n if (empty($userUuids)) {\n return [];\n }\n\n $userIds = [];\n\n foreach ($userUuids as $uuid) {\n $user = $this->userRepository->findByUuid($uuid);\n\n if (! $user || ! $user->isStatusActive()) {\n throw new InvalidArgumentException(\n sprintf('User %s not found or is not active', $uuid)\n );\n }\n\n if (! $teamCheck($user)) {\n throw new InvalidArgumentException(\n sprintf('User %s does not belong to the allowed team(s)', $uuid)\n );\n }\n\n $userIds[] = $user->getId();\n }\n\n return $userIds;\n }\n\n private function validateAndGetUserIdsByTeam(Team $team, array $userUuids): array\n {\n return $this->validateAndGetUserIds($userUuids, fn ($user) => $user->getTeamId() === $team->getId());\n }\n\n private function validateAndGetJiminnyUserIds(array $userUuids): array\n {\n $allowedTeamIds = config('kiosk.teamIds', []);\n\n return $this->validateAndGetUserIds($userUuids, fn ($user) => in_array($user->getTeamId(), $allowedTeamIds, true));\n }\n\n private function parseDate(string $dateString): string\n {\n return date('Y-m-d H:i:s', strtotime($dateString));\n }\n\n private function generateReportResultViewUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.view', ['uuid' => $result->getUuid()]);\n }\n\n private function generateReportResultDownloadUrl(AutomatedReportResult $result): string\n {\n $mediaResource = $this->getReportMediaRouteResource($result);\n\n return route('ai-reports.' . $mediaResource . '.download', ['uuid' => $result->getUuid()]);\n }\n\n private function getReportMediaRouteResource(AutomatedReportResult $result): string\n {\n if ($result->getMediaType() === self::MEDIA_TYPE_PDF) {\n return self::PDF_KEY;\n } elseif ($result->getMediaType() === self::MEDIA_TYPE_PODCAST) {\n return self::AUDIO_KEY;\n }\n\n throw new \\InvalidArgumentException('Unknown media type.');\n }\n\n public function getMediaPath(AutomatedReportResult $result): ?string\n {\n $url = match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => $result->getPdfUrl(),\n self::MEDIA_TYPE_PODCAST => $result->getPodcastAudioUrl(),\n default => null,\n };\n\n if ($url === null) {\n return null;\n }\n\n $path = parse_url(trim($url, '\"\\''), PHP_URL_PATH);\n\n return $path ?: null;\n }\n\n public function getFilenameSuffix(AutomatedReportResult $result): ?string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => 'Podcast',\n default => null,\n };\n }\n\n public function getMailSubjectSuffix(AutomatedReportResult $result): string\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PDF => 'report',\n self::MEDIA_TYPE_PODCAST => 'podcast',\n default => '',\n };\n }\n\n public function getMediaTypeMetadata(AutomatedReportResult $result): array\n {\n return match ($result->getMediaType()) {\n self::MEDIA_TYPE_PODCAST => ['extension' => 'mp3', 'mime' => 'audio/mpeg'],\n self::MEDIA_TYPE_PDF => ['extension' => 'pdf', 'mime' => 'application/pdf'],\n default => ['extension' => null, 'mime' => null],\n };\n }\n\n public function deleteS3Files(AutomatedReportResult $result): void\n {\n $teamUuid = $result->getReport()->getTeam()->getUuid();\n $reportUuid = $result->getUuid();\n\n // delete all files for a report uuid no mather of pdf, podcast, or both\n // in case of both - the podcast files are linked to the pdf (parent) uuid\n // pdf and podcast date times should be close\n $path = sprintf('%s/%s/%s', $teamUuid, self::S3_DIR, $reportUuid);\n\n foreach (self::FILE_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted S3 file', [\n 'path' => $file,\n ]);\n }\n }\n\n foreach (self::FILE_PODCAST_EXTENSIONS_VARIANTS as $extension) {\n $file = $path . '_podcast.' . $extension;\n\n if (Storage::exists($file)) {\n Storage::delete($file);\n Log::info('[Reports] Deleted Podcast S3 file', [\n 'path' => $file,\n ]);\n }\n }\n }\n\n /**\n *\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): Collection\n {\n return $this->automatedReportsRepository->getTeamIdsWithReportsResults($teamId);\n }\n\n /**\n * Core delete logic for report results using a query\n *\n * @param Builder $query\n * @param array $logContext\n *\n * @return int\n */\n private function deleteReportResultsByQuery(Builder $query, array $logContext = []): int\n {\n $deletedCount = 0;\n\n if ($query->exists()) {\n Log::info(\n 'Run delete report results',\n array_merge(\n $logContext,\n [\n 'service' => 'AutomatedReportsService',\n ]\n )\n );\n\n $query->chunkById(50, function ($results) use (&$deletedCount, $logContext) {\n foreach ($results as $result) {\n $this->deleteReportResult($result);\n $deletedCount++;\n\n Log::info(\n 'Deleted a report result',\n array_merge(\n $logContext,\n [\n 'result_id' => $result->getId(),\n 'report_id' => $result->getReportId(),\n ]\n )\n );\n }\n });\n }\n\n return $deletedCount;\n }\n\n /**\n * Delete report results for a team by retention period\n *\n * @param Team $team\n * @param CarbonImmutable $retentionDate\n *\n * @return int Number of deleted report results\n */\n public function deleteReportsResultsInRetentionPeriod(Team $team, CarbonImmutable $retentionDate): int\n {\n $reportIds = $this->automatedReportsRepository->getReportIdsByTeam($team);\n\n if ($reportIds->isEmpty()) {\n return 0;\n }\n\n $query = $this->automatedReportsRepository\n ->getReportResultsQueryForRetention($team, $retentionDate);\n\n return $this->deleteReportResultsByQuery($query, [\n 'team_id' => $team->getId(),\n 'retention_date' => $retentionDate->toDateTimeString(),\n ]);\n }\n\n /**\n * Delete ALL report results for a specific automated report\n *\n * @param string $uuid\n *\n * @return int\n */\n public function deleteReportResults(string $uuid): int\n {\n $report = $this->getReport($uuid);\n\n $query = $this->automatedReportsRepository->getResultsByReportQuery($report);\n\n return $this->deleteReportResultsByQuery($query, [\n 'report_uuid' => $uuid,\n 'report_id' => $report->getId(),\n ]);\n }\n\n public function deleteReportResult(AutomatedReportResult $result): void\n {\n $this->deleteS3Files($result);\n\n $result->delete();\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getTeamReports(Team $team): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getReportsByTeam($team);\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return \\Illuminate\\Database\\Eloquent\\Collection\n */\n public function getReportResults(AutomatedReport $report): \\Illuminate\\Database\\Eloquent\\Collection\n {\n return $this->automatedReportsRepository->getResultsByReport($report);\n }\n\n public function deleteAllReportResults(AutomatedReport $report): void\n {\n $results = $this->getReportResults($report);\n\n /** @var AutomatedReportResult $result */\n foreach ($results as $result) {\n Log::info('Deleting result', [\n 'report' => $report->getId(),\n 'result' => $result->getId(),\n ]);\n\n $this->deleteReportResult($result);\n }\n }\n\n public function deleteAllData(Team $team): void\n {\n Log::info('Deleting automated report and results for team', [\n 'team' => $team->getId(),\n ]);\n\n $reports = $this->getTeamReports($team);\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n Log::info('Deleting report', [\n 'team' => $team->getId(),\n 'report' => $report->getId(),\n ]);\n\n $this->deleteAllReportResults($report);\n\n $report->delete();\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.42785904,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.43650267,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.4474734,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.45611703,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.46476063,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.47573137,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.4867021,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"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.51329786,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.5242686,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.70611703,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"21","depth":4,"bounds":{"left":0.66921544,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.68085104,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"18","depth":4,"bounds":{"left":0.69015956,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.7017952,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.7117686,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.7287234,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"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 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 * 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 `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\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;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","depth":4,"on_screen":true,"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 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 * 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 `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\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;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Socket fail to connect to host:address=(host=localhost)(port=3306)(type=primary). Connection refused","depth":3,"bounds":{"left":0.42652926,"top":0.9577015,"width":0.29321808,"height":0.013567438},"on_screen":true,"value":"Socket fail to connect to host:address=(host=localhost)(port=3306)(type=primary). Connection refused","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1751808696220340334
|
1126710648141684156
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskAnythingPromptServiceTest
Run 'AskAnythingPromptServiceTest'
Debug 'AskAnythingPromptServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
100
3
34
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Kiosk\AutomatedReports;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Carbon\Exceptions\InvalidFormatException;
use DateTime;
use DateTimeInterface;
use DateTimeZone;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\ActivitySearch\FilterDefinition\InputTypeEnum;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Partner;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Throwable;
class AutomatedReportsService
{
public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';
public const string TYPE_ASK_JIMINNY = 'ask_jiminny';
/**
* Standard report types (used by kiosk for existing automated reports).
*/
// @TODO this will add filter, however if we need to control feature by FF we need conditional logic
public const array TYPES = [
['id' => 'exec_summary', 'name' => 'Exec Summary'],
['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],
['id' => 'product_feedback', 'name' => 'Product Feedback'],
['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],
// ['id' => 'questions', 'name' => 'Questions'],
// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],
];
public const array ALL_TYPES = [
...self::TYPES,
['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],
];
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';
/**
* Frequencies for standard (non-Ask Jiminny) reports.
*/
public const array FREQUENCIES = [
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
/**
* Frequencies for Ask Jiminny reports.
*/
public const array ASK_JIMINNY_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
];
public const string MEDIA_TYPE_PDF = 'pdf';
public const string MEDIA_TYPE_PODCAST = 'podcast';
public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];
public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];
public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];
public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];
public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];
public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];
public const int SENT_REPORT_AT_HOURS = 5;
public const string PDF_KEY = 'pdf';
public const string AUDIO_KEY = 'audio';
private const array ALL_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
private const string S3_DIR = 'reports';
private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];
private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly GroupRepository $groupRepository,
private readonly UserRepository $userRepository,
private readonly StageRepository $stageRepository,
private readonly DealStagesService $dealStagesService,
private readonly RecipientsService $recipientsService,
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly Webhook $webhookService,
private readonly BusDispatcher $dispatcher,
private readonly ActivityTypeService $activityTypeService,
private readonly PlaybookCategoryRepository $playbookCategoryRepository,
private readonly AskAnythingPromptService $askAnythingPromptService,
private readonly SearchRepository $activitySearchRepository,
private readonly AskAnythingRepository $askAnythingRepository,
) {
}
public static function getTypes(): array
{
$types = self::TYPES;
return array_map(static function ($type) {
return $type['id'];
}, $types);
}
public static function getCallTypes(): array
{
return array_map(static function ($callType) {
return $callType['id'];
}, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);
}
public static function getFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::FREQUENCIES);
}
// front-facing structure
public function getReportEnabledFieldData(bool $value = false): array
{
return [
'id' => 'report_enabled',
'label' => '',
'inputType' => InputTypeEnum::TOGGLE,
'value' => $value,
];
}
// Organizations = Teams
public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false, ?Partner $partner = null): array
{
$options = $this->getTeams(partner: $partner);
if ($shortVersion) {
return [
'id' => 'organization',
'label' => 'Organization',
'options' => $options,
];
}
return [
'id' => 'organization',
'label' => 'Organization',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $options,
'value' => $value,
'dependencies' => [
'teams',
'deal_stage_at_call',
'current_deal_stage',
'recipients',
ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,
],
'dependsOn' => [],
];
}
// Teams = Groups
public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array
{
if ($shortVersion) {
return [
'id' => 'teams',
'label' => 'Team',
'options' => $options,
];
}
return [
'id' => 'teams',
'label' => 'Team',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => false,
'placeholder' => 'Select',
'options' => $options,
'value' => $value, // value should be an array of objects {id, name}
'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],
'dependsOn' => [],
];
}
public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array
{
$types = [];
if ($team instanceof Team) {
if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
$types = self::TYPES;
}
if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {
$types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];
}
} else {
$types = self::TYPES;
}
if ($shortVersion) {
return [
'id' => 'report_type',
'label' => 'Report Type',
'options' => $types,
];
}
return [
'id' => 'report_type',
'label' => 'Report Type',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $types,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getFrequencyFieldData(?string $value = null): array
{
return [
'id' => 'frequency',
'label' => 'Frequency',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => self::FREQUENCIES,
'value' => $value,
'dependencies' => ['period'],
'dependsOn' => [],
];
}
public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array
{
return [
'id' => 'period',
'label' => 'Select one-off period',
'inputType' => InputTypeEnum::DATE_RANGE,
'required' => true,
'placeholder' => 'Select',
'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],
'queryParams' => [
'startDate' => 'start_date_period',
'endDate' => 'end_date_period',
],
'dependencies' => [],
'dependsOn' => ['frequency'],
];
}
public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array
{
return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);
}
public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);
}
public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);
}
public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'deal_value',
'label' => 'Deal Value',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_deal_value',
'max' => 'max_deal_value',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array
{
$value = [];
$conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;
$dialerOn && $value[] = self::CALL_TYPE_DIALER;
return [
'id' => 'call_type',
'label' => 'Call Type',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => [
self::CALL_TYPE_CONFERENCE,
self::CALL_TYPE_DIALER,
],
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getMediaTypeFieldData(?AutomatedReport $report = null): array
{
$value = [];
if ($report) {
$value = $this->transformMediaTypes($report);
}
return [
'id' => 'media_types',
'label' => 'Export as',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => self::MEDIA_TYPE_OBJECTS,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'call_duration',
'label' => 'Call Duration',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_call_duration',
'max' => 'max_call_duration',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getRecipientsFieldData(?Team $team = null, array $value = []): array
{
return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);
}
public function getJiminnyRecipientsFieldData(array $value = []): array
{
return $this->recipientsService->getJiminnyRecipientsFieldData($value);
}
public function getAdditionalPromptInputFieldData(?string $value = null): array
{
return [
'id' => 'additional_prompt_input',
'label' => 'Special requirements',
'inputType' => InputTypeEnum::TEXTAREA,
'required' => false,
'placeholder' => 'What should be the focus of the report?',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCustomReportNameFieldData(?string $value = null): array
{
return [
'id' => 'custom_name',
'label' => 'Custom report name',
'inputType' => InputTypeEnum::TEXT,
'required' => false,
'placeholder' => 'Enter custom name',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
// data providers
public function getTeams(?Partner $partner = null): array
{
$teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE, partner: $partner);
$teamData = [];
foreach ($teams as $team) {
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
continue;
}
$teamData[] = $this->transformTeam($team);
}
return $teamData;
}
public function getTeamGroups(string $teamUuid): array
{
$data = [];
$team = $this->getTeam($teamUuid);
if ($team !== null) {
$groups = $team->groups()->get();
foreach ($groups as $group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
];
}
}
return $data;
}
public function getTeamsGroupsOptions(array $filterTeamUuids = [], ?Partner $partner = null): array
{
$data = [];
$teams = $this->getTeams(partner: $partner);
foreach ($teams as $team) {
if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {
continue;
}
$data[] = [
'label' => $team['name'],
'groups' => $this->getTeamGroups($team['id']),
];
}
return $data;
}
public function getTeam(string $teamUuid): ?Team
{
return $this->teamRepository->idOrUuid($teamUuid);
}
public function getTeamById(int $teamId): ?Team
{
return $this->teamRepository->find($teamId);
}
public function getGroupsUuids(AutomatedReport $report): array
{
$uuids = [];
$reportGroups = $report->getGroups();
foreach ($reportGroups as $groupId) {
if ($group = $this->groupRepository->find($groupId)) {
$uuids[] = $group->getUuid();
}
}
return $uuids;
}
public function getPlaybookCategoriesUuids(AutomatedReport $report): array
{
$uuids = [];
$playbookCategories = $report->getPlaybookCategories();
foreach ($playbookCategories as $id) {
if ($category = $this->playbookCategoryRepository->find($id)) {
$uuids[] = $category->getUuid();
}
}
return $uuids;
}
public function getDealAtCallStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getDealAtCallStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getCurrentDealStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getCurrentDealStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getRecipients());
}
public function getJiminnyUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function extractUserUuids(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => $user->getUuid())
->values()
->all();
}
// get mail data
public function getRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getRecipients());
}
/**
* @return array<UserContract>
*/
public function getRecipientUserObjects(AutomatedReport $report): array
{
$userIds = $report->getRecipients()['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->values()
->all();
}
private function getJiminnyRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function buildRecipientUsers(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => [
'email' => $user->getEmailAddress(),
'name' => $user->getName(),
'timezone' => $user->getTimezone()->getName(),
])
->values()
->all();
}
public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array
{
if ($report->isAskJiminnyReport()) {
$recipients = $this->resolveAskJiminnyRecipients($report);
} else {
$recipients = $this->getRecipientUsers($report);
if ($includeJiminny) {
$recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));
}
}
$emails = [];
return array_values(array_filter(
$recipients,
static function ($recipient) use (&$emails) {
if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {
return false;
}
$emails[] = $recipient['email'];
return true;
}
));
}
private function resolveAskJiminnyRecipients(AutomatedReport $report): array
{
$recipients = [];
$creator = $report->getCreator();
if ($creator !== null) {
$recipients[] = [
'email' => $creator->getEmailAddress(),
'name' => $creator->getName(),
'timezone' => $creator->getTimezone()->getName(),
];
}
return array_merge(
$recipients,
$this->buildRecipientUsers($report->getRecipients()),
$this->getGroupRecipientUsers($report),
);
}
private function getGroupRecipientUsers(AutomatedReport $report): array
{
$users = [];
foreach ($report->getGroups() as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group === null) {
continue;
}
foreach ($group->getMembers() as $member) {
$users[] = [
'email' => $member->getEmailAddress(),
'name' => $member->getName(),
'timezone' => $member->getTimezone()->getName(),
];
}
}
return $users;
}
public function getReportTypeName(AutomatedReportResult $report): string
{
$type = $report->getReport()->getType();
$getType = $this->transformReportType($type);
return $getType['name'];
}
public function getReportPeriodName(AutomatedReportResult $report): string
{
$from = $report->getFromDate();
$to = $report->getToDate();
$frequency = $report->getReport()->getFrequency();
if ($from === null || $to === null) {
if (! $report->getReport()->isAskJiminnyReport()) {
$invalidPeriod = $from === null ? 'from' : 'to';
throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);
}
$timezone = $report->getReport()->getCreator()?->getTimezone();
$period = $this->calculateFromAndToDatePeriod($frequency, timezone: $timezone);
$from = $period['fromDate'];
$to = $period['toDate'];
}
return $this->formatReportPeriodName($frequency, $from, $to);
}
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 getReportTeamsName(AutomatedReportResult $report): string
{
$groups = $report->getGroups();
if (empty($groups)) {
return 'All';
}
// Get group names from repository
$groupNames = [];
foreach ($groups as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group) {
$groupNames[] = $group->getName();
}
}
if (count($groupNames) === 1) {
// Single team format
$teamsName = $groupNames[0];
} else {
// Multiple teams format
$teamsName = implode(', ', $groupNames);
}
return $teamsName;
}
public function getReportFileName(AutomatedReportResult $report): string
{
$customName = $report->getReport()->getCustomName();
$periodName = $this->getReportPeriodName($report);
$filenameSuffix = $this->getFilenameSuffix($report);
if ($customName) {
if ($filenameSuffix) {
$customName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$customName} - {$periodName}");
}
$baseName = $this->getReportTypeName($report);
if ($filenameSuffix) {
$baseName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}");
}
public function getReportFileNameWithExtension(AutomatedReportResult $result): string
{
$extension = $this->getMediaTypeMetadata($result)['extension'];
return $this->getReportFileName($result) . '.' . $extension;
}
public function sanitizeFileName(string $fileName): string
{
return str_replace(['/', '\\'], '-', $fileName);
}
public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool
{
$recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);
if (in_array($user->getId(), $recipientIds, true)) {
return true;
}
if ($report->isAskJiminnyReport()) {
$groupId = $user->getGroupId();
if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {
return true;
}
}
return false;
}
public function transformReportResults(Collection $automatedReportResults): array
{
$data = [];
foreach ($automatedReportResults as $automatedReportResult) {
/** @var AutomatedReportResult $automatedReportResult */
$report = $automatedReportResult->getReport();
$createdBy = $report->getCreator();
$creator = [
'id' => $createdBy?->getUuid(),
'name' => $createdBy?->getName(),
'email' => $createdBy?->getEmailAddress(),
'photoUrl' => $createdBy?->getPhotoUrl(),
];
$data[] = [
'id' => $automatedReportResult->getUuid(),
'name' => $automatedReportResult->getName(),
'frequency' => $this->transformFrequency($report->getFrequency()),
'recipients' => $this->buildRecipients($report),
'report_type' => $this->transformReportType($report->getType()),
'media_type' => $automatedReportResult->getMediaType(),
'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),
'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),
'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),
'creator' => $creator,
];
}
return $data;
}
private function buildRecipients(AutomatedReport $report): array
{
$creatorUuid = $report->getCreator()?->getUuid();
$recipients = array_values(array_filter(
$this->transformRecipients($report->getRecipients()),
static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,
));
if (! $report->isAskJiminnyReport()) {
return $recipients;
}
return [
...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),
...$recipients,
];
}
public function hasCallTypeConference(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);
}
public function hasCallTypeDialer(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);
}
// transformers
private function transformTeam(Team $team): array
{
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
return [];
}
return [
'id' => $team->getUuid(),
'name' => $team->getName(),
];
}
private function transformReportFullView(AutomatedReport $report): array
{
$base = $this->transformReportBase($report);
return $report->getType() === self::TYPE_ASK_JIMINNY
? $base + $this->transformAskJiminnyFields($report)
: $base + $this->transformStandardReportFields($report);
}
private function transformReportBase(AutomatedReport $report): array
{
return [
'id' => $report->getUuid(),
'organization' => $this->transformOrganization(team: $report->getTeam()),
'report_type' => $this->transformReportType($report->getType()),
'frequency' => $this->transformFrequency($report->getFrequency()),
];
}
private function transformStandardReportFields(AutomatedReport $report): array
{
$team = $report->getTeam();
return [
'report_enabled' => $report->getStatus(),
'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),
'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),
'deal_value_min' => $report->getDealValueMin(),
'deal_value_max' => $report->getDealValueMax(),
'call_types' => $this->transformCallType($report->getCallTypes()),
'media_types' => $this->transformMediaTypes($report),
'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),
'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),
'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),
'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),
'recipients' => $this->transformRecipients($report->getRecipients()),
'created_by' => $this->transformCreator($report->getCreator()),
'additional_prompt_input' => $report->getAdditionalPromptInput(),
'custom_name' => $report->getCustomName(),
'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),
'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),
'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),
];
}
private function transformAskJiminnyFields(AutomatedReport $report): array
{
$team = $report->getTeam();
$creatorId = $report->getAttribute('created_by');
$explicitUserIds = array_values(array_filter(
$report->getRecipients()['users'] ?? [],
static fn ($id) => $id !== $creatorId
));
return [
'report_name' => $report->getCustomName(),
'enabled' => $report->getStatus(),
'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),
'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),
'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),
'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),
];
}
private function transformOrganization(?Team $team): array
{
return [
'id' => $team?->getUuid(),
'name' => $team?->getName(),
];
}
private function transformReportType(string $type): array
{
foreach (self::ALL_TYPES as $typeItem) {
if ($typeItem['id'] === $type) {
return $typeItem;
}
}
return [];
}
private function transformCallType(array $types): array
{
$result = [];
$callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];
foreach ($types as $type) {
foreach ($callTypes as $callTypeItem) {
if ($callTypeItem['id'] === $type) {
$result[] = $callTypeItem;
break;
}
}
}
return $result;
}
private function transformMediaTypes(AutomatedReport $report): array
{
$values = [];
foreach ($report->getMediaTypes() as $mediaType) {
if (! in_array($mediaType, self::MEDIA_TYPES, true)) {
continue;
}
$values[] = match ($mediaType) {
self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,
self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,
};
}
return $values;
}
private function transformFrequency(string $frequency): array
{
foreach (self::ALL_FREQUENCIES as $frequencyItem) {
if ($frequencyItem['id'] === $frequency) {
return $frequencyItem;
}
}
return [];
}
public function transformDurationToMinutes(?int $duration): ?int
{
if (! $duration) {
return null;
}
return (int) ($duration / 60);
}
private function transformGroups(?Team $team, array $groupsIds): array
{
if (empty($groupsIds) || ! $team) {
return [];
}
$data = [];
foreach ($groupsIds as $groupId) {
$group = $team->groups()->where('id', $groupId)->first();
if ($group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
'photoUrl' => $group->getPhotoUrl(),
];
}
}
return $data;
}
private function transformStages(?Team $team, array $stagesIds): array
{
if (empty($stagesIds) || ! $team) {
return [];
}
$data = [];
foreach ($stagesIds as $stageId) {
$stage = $team->stages()->where('id', $stageId)->first();
if ($stage) {
$data[] = [
'id' => $stage->getUuid(),
'name' => $stage->getName(),
];
}
}
return $data;
}
private function transformRecipients(array $recipients): array
{
$users = [];
foreach ($recipients['users'] ?? [] as $userId) {
$users[] = $this->transformUser($userId);
}
return $users;
}
private function transformCreator(?User $user): ?array
{
if ($user === null) {
return null;
}
return $this->transformUser($user->getId());
}
private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array
{
if ($prompt === null) {
return null;
}
return [
'id' => $prompt->getUuid(),
'name' => $prompt->getTitle(),
];
}
private function transformSafeSearch(?Search $search): ?array
{
if ($search === null) {
return null;
}
return [
'id' => $search->getUuid(),
'name' => $search->getName(),
];
}
private function transformUser(int $userId): array
{
/* @var ?User $user */
$user = $this->userRepository->find($userId);
return [
'id' => $user?->getUuid(),
'name' => $user?->getName(),
'email' => $user?->getEmailAddress(),
'photoUrl' => $user?->getPhotoUrl(),
];
}
public function create(array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$validatedData['created_by'] = auth()->id();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
public function update(string $uuid, array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$report = $this->automatedReportsRepository->findByUuid($uuid);
if (! $report) {
throw new InvalidArgumentException('Report not found');
}
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
/**
* Create an Ask Jiminny report.
*/
public function createAskJiminnyReport(array $data, User $creator): array
{
$validatedData = $this->validateAskJiminnyReportData($data, $creator);
$validatedData['created_by'] = $creator->getId();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
return $this->transformReportFullView($automatedReport);
}
/**
* Update an Ask Jiminny report.
*/
public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array
{
if (! $report->isAskJiminnyReport()) {
throw new InvalidArgumentException('Report is not an Ask Jiminny report');
}
$validatedData = $this->validateAskJiminnyReportData($data, $user);
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
return $this->transformReportFullView($automatedReport);
}
public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array
{
if ($status && $report->isAskJiminnyReport() && ! $report->canExecute()) {
throw new InvalidArgumentException(
'This report is missing a saved search or prompt. ' .
'Edit the report to complete the setup before enabling it.'
);
}
$this->automatedReportsRepository->update($report, ['status' => $status]);
return $this->transformReportFullView($report->fresh());
}
/**
* Validate and transform data for Ask Jiminny reports.
*/
private function validateAskJiminnyReportData(array $data, User $user): array
{
// Validate name
$name = trim($data['report_name'] ?? '');
if (empty($name)) {
throw new InvalidArgumentException('Report name is required');
}
if (mb_strlen($name) > 50) {
throw new InvalidArgumentException('Report name must be 50 characters or less');
}
// Validate frequency (only daily, weekly, monthly for Ask Jiminny)
$frequency = $data['frequency'] ?? null;
$askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];
if (! in_array($frequency, $askJiminnyFrequencies, true)) {
throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');
}
// Validate expiration date
$expiresAt = $data['expires_on'] ?? null;
if (empty($expiresAt)) {
throw new InvalidArgumentException('Expiration date is required');
}
try {
$expiresAtDate = Carbon::parse($expiresAt);
} catch (InvalidFormatException $e) {
throw new InvalidArgumentException('Expiration date format is invalid');
}
$maxExpiration = Carbon::now()->addYear()->endOfDay();
if ($expiresAtDate->gt($maxExpiration)) {
throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');
}
if ($expiresAtDate->isPast()) {
throw new InvalidArgumentException('Expiration date cannot be in the past');
}
// Validate saved search
$activitySearchId = $data['saved_search'] ?? null;
if (empty($activitySearchId)) {
throw new InvalidArgumentException('Saved search is required');
}
$savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);
if (! $savedSearch) {
throw new InvalidArgumentException('Saved search not found or does not belong to you');
}
// Validate saved prompt
$askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;
if (empty($askAnythingPromptId)) {
throw new InvalidArgumentException('Ask Jiminny prompt is required');
}
$prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);
if (! $prompt) {
throw new InvalidArgumentException('Ask Jiminny prompt not found');
}
// Validate status
$status = $data['enabled'] ?? false;
$recipientUserIds = [$user->getId()];
if (! empty($data['share_users'])) {
$sharedUserIds = $this->validateAndGetUserIdsByTeam(
$user->team,
(array) $data['share_users']
);
$recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);
}
$sharedGroupIds = [];
if (! empty($data['share_teams'])) {
$sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);
}
$recipientUserIds = array_values(array_unique($recipientUserIds));
return [
'team_id' => $user->getTeamId(),
'type' => self::TYPE_ASK_JIMINNY,
'status' => (bool) $status,
'frequency' => $frequency,
'custom_name' => $name,
'activity_search_id' => $savedSearch->getId(),
'ask_anything_prompt_id' => $prompt->getId(),
'expires_at' => $expiresAtDate->toDateString(),
'media_types' => [self::MEDIA_TYPE_PDF],
'call_types' => [],
'recipients' => ['users' => $recipientUserIds],
'groups' => $sharedGroupIds,
];
}
public static function getAskJiminnyFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::ASK_JIMINNY_FREQUENCIES);
}
public function getAskJiminnyReportFilters(User $user): array
{
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
return [
[
'id' => 'prompt',
'label' => 'Prompt',
'options' => $prompts,
],
[
'id' => 'saved_search',
'label' => 'Saved Search',
'options' => $savedSearches,
],
];
}
public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array
{
$team = $user->getTeam();
$userTimezone = $user->getTimezone();
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
$teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [
'id' => $group->getUuid(),
'name' => $group->getName(),
])->values()->all();
$shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];
$sharedTeamsValue = [];
$sharedUsersValue = [];
if ($report) {
$sharedTeamsValue = $this->transformGroups($team, $report->getGroups());
$recipientUserIds = $report->getRecipients()['users'] ?? [];
$creatorId = $report->getAttribute('created_by');
$sharedUserIds = array_values(array_filter(
$recipientUserIds,
static fn ($id) => $id !== $creatorId
));
$sharedUsersValue = collect($sharedUserIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (User $u) => [
'id' => $u->getUuid(),
'name' => $u->getName(),
])
->values()
->all();
}
return [
'fields' => [
[
'id' => 'enabled',
'inputType' => InputTypeEnum::TOGGLE,
'label' => '',
'value' => $report?->getStatus() ?? false,
],
[
'id' => 'report_name',
'inputType' => InputTypeEnum::TEXT,
'label' => 'Name',
'placeholder' => 'Enter name',
'required' => true,
'validation' => ['maxLength' => 50],
'value' => $report?->getCustomName() ?? '',
...
|
58033
|
NULL
|
NULL
|
NULL
|
|
58036
|
2042
|
10
|
2026-05-19T11:29:45.756768+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190185756_m1.jpg...
|
PhpStorm
|
faVsco.js – ActivityController.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV (docker)APP₴82APP (-zslreturn new JsonResponse(['error'$error],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLaterMore>0.Support Daily - in 31 m100% <78• Tue 19 May 14:29:45ED→Describe what you are looking forJiminny... vscnicre# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Nikolay Yankov6 0• MessagesAdd canvas+. Direct messages8. Nikolay Yankov% Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomove Todor Stamatov "Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Newl:: AppsS Jira CloudToast1Message Nikolay Yankov+...
|
NULL
|
-5739743004679960448
|
NULL
|
click
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV (docker)APP₴82APP (-zslreturn new JsonResponse(['error'$error],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLaterMore>0.Support Daily - in 31 m100% <78• Tue 19 May 14:29:45ED→Describe what you are looking forJiminny... vscnicre# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Nikolay Yankov6 0• MessagesAdd canvas+. Direct messages8. Nikolay Yankov% Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomove Todor Stamatov "Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Newl:: AppsS Jira CloudToast1Message Nikolay Yankov+...
|
58034
|
NULL
|
NULL
|
NULL
|
|
58037
|
2043
|
11
|
2026-05-19T11:29:45.784901+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190185784_m2.jpg...
|
PhpStorm
|
faVsco.js – ActivityController.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormViewINavicarecodeKeractorTOOISWindowFV faV PhostormViewINavicarecodeKeractorTOOISWindowFV faVsco.js°9 JY-20676-delete-report-related-objectsProiect v© ActivityController.php >php api.php© AskAnythingController.php=custom.log= laravel.log4 HS_local [jiminny@localhost]& console [PROD]# console [euy© AskAnythingPromptService.php© AutomatedReportsServiceTest.phpA console [STAGING]© CoachingFeedbackCoachUserln.php› OpportunityMatcherOpportunitysyncstrategy_ ProspectsearchstrategyW service Iraitsc) client.oneC DecorateAcuivity.onpwDeleleobiecistrai.onoc)rielcDerinitions.onoC) PavloadBuillder.phpC) Profile.phpC) @uervBuilder.ohpC) @uervHandler.phpc) @uervlterator.phC) @uervResults.onoC) Service.ohrC) SvncBatchRedisService.ohoN Traits© BaseClient.php© BaseService.php© CachedCrmServiceDecorator.php© CountryCodeResolver.phpC) CrmActivitvProviderIntearatedFventl iste© AskAnythingPromptDto.phppnp apl_vz.ong© AskJiminnyReportsController.phg© AutomatedReportsService.php X C) AskAnythingPromptServiceTest.php© Search.phpclass AutomatedReportsserviceA100 У 3 У34 л v 182170217341762oumacuvilyservice.ono© CrmConfigurationSettingsService.php© crmoo ectskesolver.ong© DefaultProspectSearchStrategy.php© cmallhelper.onpurinosProspecuintenace.ongc Lavoutmanacer.onoMacchbomainbycmallintenace.ono178517801787178818181822c) OpportunityActivityMatcher.pho1828- Opportunitysyncstrateavintertace.phoc) OpportunitysyncstrateayResolver.phoC) Prospectcache.php1836c) ProsoectSearchstrateav-actorv.ono18321 ProspectSearchStrategvinterface.ohoC) ProviderReaistrv.oho@ RecordSelector.ohnT ResolveComoanvNameBvEmalltrait.oho1848C) TimePerioditerator.oho>M imnorDinternalm Kioslv M AutomatedRenortsC) ActivitvTvneService nhn© AskJiminnyReportActivityService.php1856C) AutomatedRenortsCallbackService.nhC AutomatodDonorteCorvica nho© DealStagesService.php© RecipientsService.php13 usagespublic function getGenerateReportPayload(AutomatedReport $automatedReport, string $reportResultUvi 185186SinoutPavload - =E pavload structure7 usages188 0public function getActivitiesCountPayload(array $inputPayload): array(...}190susagespublic function shouldSendReport(array $users, ?CarbonInterface $generatedAt = null): bool{...}5 usagespublic function hasPassedScheduledTime(?CarbonInterface $generatedAt, string $timezone): boolf...} 195—194196b usagespublic function calculateFromAndToDatePeriod(string strequency?Carbon $fromDate = null,rlarbon scouare = null.DateTimeZonelstring|null Stimezone = null: array {...}=1982001 usageprivate function calculateFromAndToDate(AutomatedReport SautomatedReport): array{...}public function aetAskJiminnvGenerateReportPavloaddAutomatedRenortResult SreportResult.arrav Sactivitvids.•: array &...2 usadedprivate function getCallbackUrl: stringf...}* Validate and transform payload data for automated reports* @param array $data* @throws InvalidArgumentException202203204205206207-208209210211212213-214215FtEE220222223* areturn arrayprivate function validateAndTransformData(array $data): array{...}So jiminny v021 A1 A18 V.2 Y6 ^SELECT ar.id, ar.uuid, ar.media. type, ar.status, a.typeFROM automated_report_results arJOIN automated_reports a ON a.id = ar.report. idWHERE a.type ='ask_jiminny'LIMIT 10;SELECT * FROM automated_reports where id = 71:SELECT * FROM automated_reresults where report id = 71:UPDATE automated_reports set playbook categories = NULL where id = 68SELECT * FROM automated report results where id = 275;SaleL * rkum auromaredSELECT * FROMt results order by 1d descselect * from activity_searches where user_id = 143;select * fronSELECTINNER JOIN automated_reportsautomated renort results' "renort id' = 'automated renorts' 'idiANDautomated renort results' 'aenerated at' IS NOT NULL'automated_report_results'. 'sent_at* IS NOT NULLANDautomated renorts' 'team id = 1AND JSON_CONTAINS(' automated_reports'. 'recipients', 143, '$."users"')SELECT * FROM automated_reports where id = 67;SELECT * FROM automated_reports where id = 42;SELECT * FROM users WHERE id = 143; # group 28select * from teams where id = 3143;select * from crm_ configurations where id = 500;select * from users where name = 'Integration Account': # 1695SELECT * FROM social accounts WHERE sociable_ id = 1695:select * from activities where crm_configuration id = 39and recording state = 'recorded' and duration > 60and status = 'completed' and actual start time >= '2025-12-01':SELECT * FROM activities WHERE uuid to bin(•458cf915-6914-4000-b083-568763262956') = uuid:select * from leads›SELECT * FROM activities WHERE uuid to bin('{43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid: # 422003SELECT * FROM activities WHERE id IN (16,422003):SSIECT * SROM activitios whenp status =Socket fail to connect to host:address=(host=localhost) (port=3306)(tvpe=primarv). Connection refusedhel"suppont Dally • In 31m100% 5• Tue 19 May 14:29:45AskAnythingPromptServiceTest+0 ..CascadeAsk Jiminny Report M$shareUsers,SSharebroups,Soromot->qetHaskeportsorInat's correct and already there. Tnanks lo the loadcount in the repository, this is now olt extra query for tne whole list.Whar apour edttoredit(") currentlv doesn't set hasRenorts. which is fine and intentional:• The newly created prompt (in the isDefaultPrompt branch) is fresh - by definition zero reports.• The edited prompt may or may not have reports attached, but: editing the prompt doesn't add or remove reportassociations, so the frontend already knows the value from when the prompt was loaded in the list. The PUT responsedoesn't need to recompute itIf vou want the response to be consistent with create() (which I chanaed to exolicitlv send false ). vou have two ontions.1. Leave edito as-is— has renorts lis simolv absent from the resnonse. Cleanest semanticaliv. "we didn't touch this2. Set hasRenorts: SnewPromot->aetHasRenorts() —falls back to an exists() query (since loadCount wasn't run onlIf vou want ontion 2. here's the chanae1AckAnvthinaPromntService.nhn-152-161return new AskAnvthingPromntDtolSnewP rompt-getcontentl,SnewPromot->getlargeto.sshareGroupsUuidsMy recommendation: leave edit alone. The trontend already nas the value trom the list, and skippina it avoids anunnecessarv DB hit on everv edit. The create() case aot false onlv because a brand-new promot is triviallv known totests/Unit/Component/AskAnvthing/B AskAnvthingPromptServiceTest.php +25-2app/Component/AskAnything/@ AskAnythingPromptService.php +7 -2app/Repositories/ @ ASKAnythingRepository.php +7 -1ann/Httn/Trancformore/M SoarchTrancformor.nhnt1lers/API/ ActivityController.php +1/m AskAnvthinaPromot.oho +4Ask anvthina (&4-L)<> Code Claude Qnus 1.7 Mediumal "View allAccent alliWN Windsurf Toams 1787-1UTE.8Po 4 spaces...
|
NULL
|
8500074505157123594
|
NULL
|
click
|
ocr
|
NULL
|
PhostormViewINavicarecodeKeractorTOOISWindowFV faV PhostormViewINavicarecodeKeractorTOOISWindowFV faVsco.js°9 JY-20676-delete-report-related-objectsProiect v© ActivityController.php >php api.php© AskAnythingController.php=custom.log= laravel.log4 HS_local [jiminny@localhost]& console [PROD]# console [euy© AskAnythingPromptService.php© AutomatedReportsServiceTest.phpA console [STAGING]© CoachingFeedbackCoachUserln.php› OpportunityMatcherOpportunitysyncstrategy_ ProspectsearchstrategyW service Iraitsc) client.oneC DecorateAcuivity.onpwDeleleobiecistrai.onoc)rielcDerinitions.onoC) PavloadBuillder.phpC) Profile.phpC) @uervBuilder.ohpC) @uervHandler.phpc) @uervlterator.phC) @uervResults.onoC) Service.ohrC) SvncBatchRedisService.ohoN Traits© BaseClient.php© BaseService.php© CachedCrmServiceDecorator.php© CountryCodeResolver.phpC) CrmActivitvProviderIntearatedFventl iste© AskAnythingPromptDto.phppnp apl_vz.ong© AskJiminnyReportsController.phg© AutomatedReportsService.php X C) AskAnythingPromptServiceTest.php© Search.phpclass AutomatedReportsserviceA100 У 3 У34 л v 182170217341762oumacuvilyservice.ono© CrmConfigurationSettingsService.php© crmoo ectskesolver.ong© DefaultProspectSearchStrategy.php© cmallhelper.onpurinosProspecuintenace.ongc Lavoutmanacer.onoMacchbomainbycmallintenace.ono178517801787178818181822c) OpportunityActivityMatcher.pho1828- Opportunitysyncstrateavintertace.phoc) OpportunitysyncstrateayResolver.phoC) Prospectcache.php1836c) ProsoectSearchstrateav-actorv.ono18321 ProspectSearchStrategvinterface.ohoC) ProviderReaistrv.oho@ RecordSelector.ohnT ResolveComoanvNameBvEmalltrait.oho1848C) TimePerioditerator.oho>M imnorDinternalm Kioslv M AutomatedRenortsC) ActivitvTvneService nhn© AskJiminnyReportActivityService.php1856C) AutomatedRenortsCallbackService.nhC AutomatodDonorteCorvica nho© DealStagesService.php© RecipientsService.php13 usagespublic function getGenerateReportPayload(AutomatedReport $automatedReport, string $reportResultUvi 185186SinoutPavload - =E pavload structure7 usages188 0public function getActivitiesCountPayload(array $inputPayload): array(...}190susagespublic function shouldSendReport(array $users, ?CarbonInterface $generatedAt = null): bool{...}5 usagespublic function hasPassedScheduledTime(?CarbonInterface $generatedAt, string $timezone): boolf...} 195—194196b usagespublic function calculateFromAndToDatePeriod(string strequency?Carbon $fromDate = null,rlarbon scouare = null.DateTimeZonelstring|null Stimezone = null: array {...}=1982001 usageprivate function calculateFromAndToDate(AutomatedReport SautomatedReport): array{...}public function aetAskJiminnvGenerateReportPavloaddAutomatedRenortResult SreportResult.arrav Sactivitvids.•: array &...2 usadedprivate function getCallbackUrl: stringf...}* Validate and transform payload data for automated reports* @param array $data* @throws InvalidArgumentException202203204205206207-208209210211212213-214215FtEE220222223* areturn arrayprivate function validateAndTransformData(array $data): array{...}So jiminny v021 A1 A18 V.2 Y6 ^SELECT ar.id, ar.uuid, ar.media. type, ar.status, a.typeFROM automated_report_results arJOIN automated_reports a ON a.id = ar.report. idWHERE a.type ='ask_jiminny'LIMIT 10;SELECT * FROM automated_reports where id = 71:SELECT * FROM automated_reresults where report id = 71:UPDATE automated_reports set playbook categories = NULL where id = 68SELECT * FROM automated report results where id = 275;SaleL * rkum auromaredSELECT * FROMt results order by 1d descselect * from activity_searches where user_id = 143;select * fronSELECTINNER JOIN automated_reportsautomated renort results' "renort id' = 'automated renorts' 'idiANDautomated renort results' 'aenerated at' IS NOT NULL'automated_report_results'. 'sent_at* IS NOT NULLANDautomated renorts' 'team id = 1AND JSON_CONTAINS(' automated_reports'. 'recipients', 143, '$."users"')SELECT * FROM automated_reports where id = 67;SELECT * FROM automated_reports where id = 42;SELECT * FROM users WHERE id = 143; # group 28select * from teams where id = 3143;select * from crm_ configurations where id = 500;select * from users where name = 'Integration Account': # 1695SELECT * FROM social accounts WHERE sociable_ id = 1695:select * from activities where crm_configuration id = 39and recording state = 'recorded' and duration > 60and status = 'completed' and actual start time >= '2025-12-01':SELECT * FROM activities WHERE uuid to bin(•458cf915-6914-4000-b083-568763262956') = uuid:select * from leads›SELECT * FROM activities WHERE uuid to bin('{43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid: # 422003SELECT * FROM activities WHERE id IN (16,422003):SSIECT * SROM activitios whenp status =Socket fail to connect to host:address=(host=localhost) (port=3306)(tvpe=primarv). Connection refusedhel"suppont Dally • In 31m100% 5• Tue 19 May 14:29:45AskAnythingPromptServiceTest+0 ..CascadeAsk Jiminny Report M$shareUsers,SSharebroups,Soromot->qetHaskeportsorInat's correct and already there. Tnanks lo the loadcount in the repository, this is now olt extra query for tne whole list.Whar apour edttoredit(") currentlv doesn't set hasRenorts. which is fine and intentional:• The newly created prompt (in the isDefaultPrompt branch) is fresh - by definition zero reports.• The edited prompt may or may not have reports attached, but: editing the prompt doesn't add or remove reportassociations, so the frontend already knows the value from when the prompt was loaded in the list. The PUT responsedoesn't need to recompute itIf vou want the response to be consistent with create() (which I chanaed to exolicitlv send false ). vou have two ontions.1. Leave edito as-is— has renorts lis simolv absent from the resnonse. Cleanest semanticaliv. "we didn't touch this2. Set hasRenorts: SnewPromot->aetHasRenorts() —falls back to an exists() query (since loadCount wasn't run onlIf vou want ontion 2. here's the chanae1AckAnvthinaPromntService.nhn-152-161return new AskAnvthingPromntDtolSnewP rompt-getcontentl,SnewPromot->getlargeto.sshareGroupsUuidsMy recommendation: leave edit alone. The trontend already nas the value trom the list, and skippina it avoids anunnecessarv DB hit on everv edit. The create() case aot false onlv because a brand-new promot is triviallv known totests/Unit/Component/AskAnvthing/B AskAnvthingPromptServiceTest.php +25-2app/Component/AskAnything/@ AskAnythingPromptService.php +7 -2app/Repositories/ @ ASKAnythingRepository.php +7 -1ann/Httn/Trancformore/M SoarchTrancformor.nhnt1lers/API/ ActivityController.php +1/m AskAnvthinaPromot.oho +4Ask anvthina (&4-L)<> Code Claude Qnus 1.7 Mediumal "View allAccent alliWN Windsurf Toams 1787-1UTE.8Po 4 spaces...
|
58033
|
NULL
|
NULL
|
NULL
|
|
58038
|
2042
|
11
|
2026-05-19T11:29:49.095031+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190189095_m1.jpg...
|
PhpStorm
|
faVsco.js – ActivityController.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV (docker)APP₴82APP (-zslreturn new JsonResponse(['error'$error],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLaterMore>0.Support Daily - in 31 m100% <7Tue 19 May 14:29:48ED→Describe what you are looking forJiminny... vscnicre# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Nikolay Yankov6 0• MessagesAdd canvas+. Direct messages8. Nikolay Yankov% Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomove Todor Stamatov "Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Newl:: AppsS Jira CloudToast1Message Nikolay Yankov+...
|
NULL
|
-3792104286666472920
|
NULL
|
click
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV (docker)APP₴82APP (-zslreturn new JsonResponse(['error'$error],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLaterMore>0.Support Daily - in 31 m100% <7Tue 19 May 14:29:48ED→Describe what you are looking forJiminny... vscnicre# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Nikolay Yankov6 0• MessagesAdd canvas+. Direct messages8. Nikolay Yankov% Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomove Todor Stamatov "Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Newl:: AppsS Jira CloudToast1Message Nikolay Yankov+...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58039
|
2043
|
12
|
2026-05-19T11:29:50.707371+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190190707_m2.jpg...
|
PhpStorm
|
faVsco.js – SearchTransformer.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskAnythingPromptServiceTest
Run 'AskAnythingPromptServiceTest'
Debug 'AskAnythingPromptServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.10405585,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects<br/>Some incoming commits are not fetched<br/>","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.8194814,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskAnythingPromptServiceTest","depth":6,"bounds":{"left":0.83477396,"top":0.019952115,"width":0.080784574,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskAnythingPromptServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskAnythingPromptServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"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},"on_screen":true,"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},"on_screen":true,"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},"on_screen":true,"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},"on_screen":true,"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},"on_screen":false,"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},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-6579782958979142838
|
-3772662980223977080
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskAnythingPromptServiceTest
Run 'AskAnythingPromptServiceTest'
Debug 'AskAnythingPromptServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
PhostormViewINavicarecodeLaravelKeractorWindowmelpFV faVsco.js?9 JY-20676-delete-report-related-objectsroledeyC ActivityController.ong© SearchTransformer.php X php api.php© AskAnythingController.php(C) CoachinaSectionFeedbacksTransformen© AskAnythingPromptService.php© AutomatedReportsServiceTest.php(C) CoachingsectionsIranstormer.onp© CoachingSectionTransformer.php© CommentTransformerAbstract.php© AskAnythingPromptDto.php© AskJiminnyReportsController.phg© AutomatedReportsService.php©) AskAnythingPromptServiceTest.php© Search.phpoconnection transtormer.onp<?phpocontacllranstormer.one© CrmTemplateFieldTransformer.phpnamespace Jiminny Http Transtormers:c crmiemplatelranstormer.onp© CrmTransformer.php›use ...Datalranstormer.onpc DealstageIranstormer.pnpclass Searchtransformer extends TransformerAbstractc) reaturelranstormer.pnpc) rieldIranstormer.onoprotected arrav Savailableincludes =c) rielcvalue lranstormer.onoC) ForecastTranstormer.ohpc) GroupDealRiskTvoeTranstormer.oho© GroupTransformer.php23 6protected array $defaultIncludes = [©Intearationtiranstormer.oho© InvitationTransformer.ohpc) ,oo tetranstormer.ono© LanquageDialectTransformer.ohpprivate User $consumer:© LayoutEntityTransformer.php©LayoutTransformer.phpC) Leadtransformer.oho@ MessageTransformer.ohopUbLic Tunccion__construct(privare readonly Aculvitysearch saculvlcysearch,privace readonly nuacelranstormer snuccelranstorner.© MomentTransformer.php© NoteTransformer.php© NotificationTransformer.php© OnDemandActivitiesTransformer.php© OpportunityCommentTransformer.php© OpportunityTransformer.php(c) Particinantranstormer.ongprivate readonly Loggerintertace Slogger.D{...}public function withConsumer(User Suser): selfsthas->consumer = suser:(e) DartnerTranstormer.ong© PermissionTransformer.php© PlaybackPageTransformer.php© PlaybookTransformer.php© PlaybookTreeTransformer.phpPlavlistActivitvTransformer.phpPlavlistSharesTransformer.php@ PlavlistTransformer.phppublic function transformSearch Ssearch: arravreturn"id' = Ssearch->id string.'namel => Ssearch->aetNameOl.c)Prosoectsfranstormer.ono'filters' => $this->getFilters($search),'has reports' => Ssearch->automatedReports-›existso.Accent ReiectC) RecordTivoeTransformer.ohoC) RoleTransformer.oho'has reports' => ((int) (Ssearch->automated reports count ?? 0)) > 0.C) SconeGrouotiransformer.ohoC) ScorecardRuleTiransformer.ohoC) ScorecardRuleTriagerTransformer.oho(c) Scorecardtiransformer nhnlpublic function includeNudges(Search Ssearch): Fractal\Resource\Collectionf...}(C) Saccion Transformer nhnpublic function getFilters(Search Ssearch): arrav{...?l(C) StadeTransformer nhnX Reject File 4%€ + 1 of 5 files →suppont Dally • In 31m100% 5• Tue 19 May 14:29:50AskAnythingPromptServiceTest+0 ..=custom.log= laravel.log4 HS_local [jiminny@localhost]& console [PROD]# console [euyA console [STAGING] © CoachingFeedbackCoachUserln.phpSo jiminny v021 A1 A18 V.2 Y6 ^SELECT ar.ig, ar.wid, ar.nedidatype, ar.status, a.tYRgFROM automated_report_results arJOIN automated_reports a ON a.id = ar.report. idWHERE a.type ='ask_jiminny'LIMIT 10;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 = 68SELECT * FROM automated report results where id = 275;SELECT * FROM automated_reports order by id desc;SELECT * FROMautomated report results order by id desc:select * from activity_searches where user_id = 143;select * fronSELECT'automated report results'.* FROM 'automated report resultsiautomated renort results' "renort id' = 'automated renorts' 'idiANDautomated_report_results'.'generated_at' IS NOT NULL'automated_report_results'. 'sent_at* IS NOT NULLAND JSON_CONTAINS( automated_reports'. 'recipients', 143, '$."users""')SFLECT * EPOM automated nenonts whenp id = 47-SFLECT * SPOM automated nenonts whene id = 12.SELECT * FROM users WHERE id = 143; # group 28select * from teams where id = 3143;select * from crm_configurations where id = 500:select * from users where name = 'Integration Account': # 1695SELECT * FROM social accounts WHERE sociable id = 1695:select * from activities where crm_ configuration id = 39and recording state = 'recorded' and duration > 60and status = 'completed' and actual start time >= '2025-12-01':SELECT * FROM activities WHERE uuid to bin('458cf915-6914-4000-b083-568763262956') = uuid:select * from leads›SELECT * FROM activities WHERE uuid to bin('{43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid: # 422003SELECT * FROM activities WHERE id IN (16.422003);SSIECT * SROM activitios whenp status =Socket fail to connect to host:address=(host=localhost) (port=3306)(tvpe=primarv). Connection refusedCascadeAsk Jiminny Report Msowneruuzo,$shareGroups,Sprompt->getHasReports(),That's correct and already there. Thanks to the loadCount in the repository, this is now O(1) extra query for the whole list.What about edit()?edito currently doesn't set hasRenorts, which is tine and intentional:• The newlv created oromot lin the isDetaul tPromot branch) is tresh — ov definition zero revoris• The edited prompt may or may not have reports attached, but: editing the prompt doesn't add or remove reportassociations, so the frontend already knows the value from when the promot was loaded in the list. The PUt responsedoesn't need to recompute itIf vou want the response to be consistent with create (which I changed to exolicitly send false ), vou have two options.1. Leave edit() as-is— has_reports is simply absent from the response. Cleanest semantically: "we didn't touch thisdon't trust whatever you tind here."2. Set hasReports: $newPrompt->getHasReports() — falls back to an exists() query (since loadCount wasn't run onthe single prompt). One extra cheap query per edit. Keeps the AP response shape unitorm with the list endpolntIf vou want option 2, here's the chanaeAskAnvthinareturn new AskAnvthinaPromotDtodSnewrronpt→deruuldnSnewPromnt_snetContentoSShareusersuuzasMy recommendation: leave edit alone. The frontend already has the value from the list, and skipping it avoids anunnecessary DB hit on every edit. The create@ case aot false onlv because a brand-new oromot is trivially known toG Hlocwith Ahonnoeent/AskAnything/@ AskAnythingPromptServiceTest.php +25-2app/Component/AskAnything/ AskAnythingPromptService.php +7 -2ann/Ponocitorioc/ M.AckAnvtbinaPonocitorv.nhnt7.rs/ SearchTransformer.php +1 -1/m AskAnvthinaPromot.oho +4Ask anvthina (884-L)<> Code Claude Qnus 1.7 MediumView all* Reiect alliiAccent allWN Windsurf Teams48-70 (22 charc)io 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58040
|
2042
|
12
|
2026-05-19T11:29:52.993019+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190192993_m1.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsServiceTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV (docker)APP₴82APP (-zslreturn new JsonResponse(['error'$error],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLaterMore>0.Support Daily - in 31 m100% <78• Tue 19 May 14:29:52ED→Describe what you are looking forJiminny... vscnicre# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Nikolay Yankov6 0• MessagesAdd canvas+. Direct messages8. Nikolay Yankov% Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomove Todor Stamatov "Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Newl:: AppsS Jira CloudToast1Message Nikolay Yankov+...
|
NULL
|
8399351822806707111
|
NULL
|
click
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV (docker)APP₴82APP (-zslreturn new JsonResponse(['error'$error],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLaterMore>0.Support Daily - in 31 m100% <78• Tue 19 May 14:29:52ED→Describe what you are looking forJiminny... vscnicre# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Nikolay Yankov6 0• MessagesAdd canvas+. Direct messages8. Nikolay Yankov% Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomove Todor Stamatov "Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Newl:: AppsS Jira CloudToast1Message Nikolay Yankov+...
|
58038
|
NULL
|
NULL
|
NULL
|
|
58041
|
2043
|
13
|
2026-05-19T11:29:54.623550+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190194623_m2.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskAnythingPromptServiceTest
Run 'AskAnythingPromptServiceTest'
Debug 'AskAnythingPromptServiceTest'
More Actions...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.10405585,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects<br/>Some incoming commits are not fetched<br/>","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.8194814,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskAnythingPromptServiceTest","depth":6,"bounds":{"left":0.83477396,"top":0.019952115,"width":0.080784574,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskAnythingPromptServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskAnythingPromptServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"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},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4974519233973743475
|
-4022622654610547319
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskAnythingPromptServiceTest
Run 'AskAnythingPromptServiceTest'
Debug 'AskAnythingPromptServiceTest'
More Actions
PhostormroledeyVIewINavicarecodeFV faVsco.js°9 JY-20676-delete-report-related-objectssuppont Dally • In 31mAskAnythingPromptServiceTest v100% 5• Tue 19 May 14:29:54© AskAnythingController.phpcustom.log= laravel.log4 SF jiminny@localhost] X4 HS_local [jiminny@localhost]& console [PROD]# console [euy> • OpportunitySyncStrategy• • ProspectSearchStrategy• M ServiceTraits© ClientTest.phpC) DecorateActivitylest.pnpC DeleteObjectsTraitTest.php© FieldDefinitionsTest.phpC GetActivityFieldNameTest.phpPayloadBuilderTest.phpC QueryBuilderTest.phpC) QueryHandlerTest.phpg) @uerviteratorlest.onp© QuervResultsTests.php(c) service lest.onoC SvncBatchRedisServiceTest.php© AskAnythingPromptService.php© AutomatedReportsServiceTest.php X# concole ISTAGinG1C) CoachinaFeedhackCoachl.Icerin.nhnPlavaround+0 ..C AsKJiminnyкepс341 P©) CrmActivitvServicelest.onoc) crmobiectsResolveriiest.onoc DefaultProsnectSearchStrateavTest.cc EmailHelperTest.oho© FieldValueConverterTest.phpc LavoutManaderTest.oho© MigrateProviderServiceTest.php© OpportunityActivityMatcherTest.php© OpportunitySyncStrategyResolverTes© ProspectCacheTest.php© ProspectSearchStrategyFactoryTest.pC ProviderPeaistrvTect nhnl© RecordSelectorTest.phpaDocolwoCamnanyNamoRySmailTroitt©) TimePeriodlteratorTest.php© UpdateCrmDataResolverTest.php> C Internalv @ Kioskv @ AutomatedReports© ActivitvTvpeServiceTest.php© AskJiminnyReportActivitvServiceT© AskJiminnyReportServiceTest.pho(c) AutomatedRevortscalllbackServiceC AutomatedReportsServiceActivitieC) AutomatedRenortsServiceExtende@ AutomatedReportsServiceGetActivC) AutomatedRenortsServiceGetPlavi(ch AutomatedRenortsServiceRenortech AutomatedRenortsServiceValidateCh AutomatedRenortsServiceValidatiopublic function testTransformReportResultsStructure: voidt...}— 184~ 185194public function testTransformReportResultsWithMultipleResults: void{...}::188 ©#Dacarrovider1sUserkec1p1encutkeporcbacarrov1der'upusizo tunctzon testisUserRecâpientOfReport(ánt SuserId, array Srecipients, bool Sexpected): vosd 90#[Datarrovider 1sUserkec1!ReportDataProvider')]public function testIsUserRecipient0fAskJiminnyReportViaGroup(int suserldPint SaroupId.larray Srecipients.bool Sexpected.•: vo1d <..."nublic function testIsllserRecinient0fNonAsk.JiminnvRenortIanoresGrouns@• voidf..}=194195— 19*=198200-2041 usagenublic static function isllserRecinient0fAsk.JiminnvRenortDataProvider(: arravf.public function testIsUserRecipient0fReportWithEmptyRecipients(): void{...}public function testIsUserRecipient0fReportWithNoUsersKey(): voidf...}=205294•201— ая.-209public static function isUserRecipient0fReportDataProvider(): arrayf...}private function createMockReportResult(string $uuid = 'test-uuid', string $reportType = 'exec_sum#[DataProvider('getUsersUuidsDataProvider')]public function testGetUsersUuids(array Srecipients, array SmockUsers, array SexpectedUuids): voidpublic function testGetUsersUuidsWithEmptyRecipients@: void{...}public function testGetUsersUuidsWithNoUsersKey@: void{...}221= 22?nublic function testGetlsersluidsWithNonExistentUsers(: voidf...}So jiminny vm 021 A1 A18 V2 V6 ^ VCascadeAsk Jiminny Report MSprompt→>getcontent(),StardetCownerlluid.SELECT ar.id, ar.uuid, ar.media. type, ar.status, a.typeER0M automated nenont neculte anJOIN automated_reports a ON a.id = ar.reportidWHERE a.type = 'ask_jiminny'ITMTT 10-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 * FROMautomated report results order by id desc:select * from activity searches where user_ id = 143:select * fronSELECT'automated report results'.* FROM 'automated report resultsiINNER JOIN 'automated reportsautomated renort results' 'renort id' = 'automated renorts' 'id'automated renort results' 'aenerated at' IS NOT NULLAND 'automated renont results' 'sent at' is NOt NuLlautomated renorts' 'team id = 1AND ISON CONTATNS('automated nenonte' 'reciniente'. 14? Ig lucencil)SELECT * EPOM automated nenonts whene id = 47•SELECT * FROM automated_reports where id = 42;colont + fnom toome wbono id = 71/2.SELECT * FROM social accounts WHERE sociable_ id = 1695:and status = 'completed' and actual start time >= '2025-12-01':ssndreuroups,Sprompt→>getHasReports),That's correct and already there. Thanks to the loadCount in the repository, this is now O(1) extra query for the whole list.What ahout edit/2edito currently doesn't set hasReports, which is fine and intentional:• The newly created prompt (in the isDefaultPrompt branch) is fresh - by definition zero reports.• The edited nromnt mav or mav not have renorts attached. but. editina the nromnt doesn't add or remove renortdoesn't need to recompute ilIf vou want the response to be consistent with create which I changed to explicitly send false ), vou have two options:1. Leave edito as-is— has reports is simolv absent from the response. Cleanest semanticallv: "we didn't touch thisdon't trust whatever you find here."2. Set hasReports: SnewPrompt->qetHasReportso -talls back to an exists query since loadCount wasn't run onthe single prompt). One extra cheap query per edit. Keeps the API response shape uniform with the list endpoint.return new AskAnvthinaPromotdtolSnewPronnt_snetlluidtSnewPrompt->getlargetonCcha rellcerclimidsMv recommendation: leave edit alone. The frontend alreadv has the value from the list and ckinnina it avoids anunnecessary DB hit on every edit. The create() case aot false onlv because a brand-new oromot is trivially known tonave no repons — no query cost.nl .SELECT * FROM activities WHERE uuid to bin('458cf915-b914-4000-b083-568763262956') = uuidiselect * from leads›4 tiles with changesvnent/AskAnything/ AskAnythingPromptServiceTest.php +25-2annlComnonont/AckAnvtbina/M.AckAnvthinaDromntSorvico.nhnt7.ies/ D AskAnythingRepository.php +7 -/M AskAnvthinaPromot.ohn +4* Reiect alliiAccent all52 VSELECT * FROM activities WHERE uuid to bin('{43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid: # 422003nublic static function aetlserslluidsDataProvider@• arravf..?SELECT * FROM activities WHERE id IN (16.422003);SFLECT + EPOM activities where statud#DataProvider('detCurrentDealStagesUuidsDataProviden')1oubtie funetion tectfethurnentReel Staneclluidefannav Sonnentheal Stenee annay SanckStagee annanESocket fail to connect to host:address=(host=localhost) (port=3306)(tvpe=primarv). Connection refused+ (todav 12-17)Ask anvthing 884-D)<> Code Claude Qnus 4.7 MediumWN Windsurf Teams212-22 LITC.8io 4 spaces...
|
58039
|
NULL
|
NULL
|
NULL
|
|
58042
|
2042
|
13
|
2026-05-19T11:29:56.903435+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190196903_m1.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsServiceTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV (docker)APP₴82APP (-zslreturn new JsonResponse(['error'$error],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLaterMore>0.Support Daily - in 31 m100% <7Tue 19 May 14:29:56ED→Describe what you are looking forJiminny... vscnicre# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Nikolay Yankov6 0• MessagesAdd canvas+. Direct messages8. Nikolay Yankov% Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomove Todor Stamatov "Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Newl:: AppsS Jira CloudToast1Message Nikolay Yankov...
|
NULL
|
454450194757748238
|
NULL
|
click
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV (docker)APP₴82APP (-zslreturn new JsonResponse(['error'$error],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLaterMore>0.Support Daily - in 31 m100% <7Tue 19 May 14:29:56ED→Describe what you are looking forJiminny... vscnicre# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Nikolay Yankov6 0• MessagesAdd canvas+. Direct messages8. Nikolay Yankov% Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomove Todor Stamatov "Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Newl:: AppsS Jira CloudToast1Message Nikolay Yankov...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58043
|
2043
|
14
|
2026-05-19T11:30:00.192519+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190200192_m2.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingPromptServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskAnythingPromptServiceTest
Run 'AskAnythingPromptServiceTest'...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.10405585,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects<br/>Some incoming commits are not fetched<br/>","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.8194814,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskAnythingPromptServiceTest","depth":6,"bounds":{"left":0.83477396,"top":0.019952115,"width":0.080784574,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskAnythingPromptServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4959269709546757893
|
-8637481863596221053
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskAnythingPromptServiceTest
Run 'AskAnythingPromptServiceTest'
PhostormProiect vVIewINavicarecodeWindowFV faVsco.js°9 JY-20676-delete-report-related-objectsC ActivityController.ong© AskAnythingController.php=custom.log= laravel.log4 SF jiminny@localhost] x4 HS_local [jiminny@localhost]> • OpportunitySyncStrategy• • ProspectSearchStrategy> • ServiceTraits© ClientTest.php©) DecorateActivitylest.onpC DeleteObjectsTraitTest.php© FieldDefinitionsTest.phpC GetActivityFieldNameTest.phpPayloadBuilderTest.php@ QueryBuilderTest.phpC) QueryHandlerTest.php© QuerylteratorTest.php© QuervResultsTests.php(c) service lest.onoC SvncBatchRedisServiceTest.php©) BaseServiceTest.ohoc) CachedcrmServiceDecoratorTest.ohrc) CrmActivitvServiceTlest.onoc) crmcontidurationSettinasServicetiestc) crmobiectsResolveriiest.ono© DefaultProspectSearchStrategyTest.pc EmailHelperTest.ohoc) FieldValueConverterTest.ohoc) LavoutManaderTest.oho© MigrateProviderServiceTest.php© OpportunityActivityMatcherTest.php© OpportunitySyncStrategyResolverTes© ProspectCacheTest.php@ ProspectSearchStrategyFactoryTest.fC ProviderPeaistrvTect nhnl© RecordSelectorTest.phpaDocolwoCamnanyNamoRySmailTroitt.©) TimePeriodlteratorTest.php© UpdateCrmDataResolverTest.php> C Internalv @ Kioskv @ AutomatedReports© ActivitvTvpeServiceTest.php© AskJiminnyReportActivitvServiceT© AskJiminnyReportServiceTest.php@ AutomatedReportsCallbackServiceC AutomatedReportsServiceActivitie.C) AutomatedRenortsServiceExtende@ AutomatedReportsServiceFieldDat© AskAnythingPromptService.php© AutomatedReportsServiceTest.phpA console [STAGING]C) CoachinaFeedhackCoachl.Icerin.nhn© AskAnythingPromptDto.php© AskJiminnyReportsController.php©) AskAnythingPromptServiceTest.php x © Search.phpdeclare(strict tyoes=1):namespace Tests\Unit\Component\AskAnythingust...cllass AskAnvthinaPromn+Servicetest extends Testcase31 đ ›(C) AutomatedRenortsServiceFormatti@ AutomatedReportsServiceGetActivC) AutomatedRenortsServiceGetPlavt( AutomatedRenortcServiceRenortGuse TestPnivateMethod:14 usages31 usagesprivate askanychingrromprservice saskanychingrromptservice.private askanychingkepostcorydockubnect saskAnychingkepostcoryprivare userkeposicoryxrockubect suserkeposicory.private broupkepos1torylntertacexrockubnect sqroupkepos1tory*protected function setUp0: void{...}nubuic tunction testbetaskanvthinoPromotso: vozdisuser = sthis->createrock originalClassName: User:: class)Suser->expects(Sthis->anv0)'aetid')->wilReturn( value: 0 :susen->exnects(Sthis->anvobi'aetluid')119)9SdefaultPromot = Sthis->createMock@ oricinalClassName:AskAnvthinaPromnt.•class)•SdefaultPromot->exnects(Sthis->onceonl'getOwner')->wil1Returnd value: nulb.$defaultPrompt->expects(Sthis->onceO)->method( constraint: 'getUuid')->willReturn( value: 'default-uuid'):$defaultPrompt->expects(Sthis->onceO)morcaloiall cencttatinigetiaitler)-swil1Potunnt value'Sentiment')$defaultPrompt->expects(Sthis->onceO): 'getContent')->W1LLReturn value:'What was the overall sentiment of the customer during the call?'):$defaultPromot->expects(Sthis->onceO) —213215216217218219220-221222223225ci AutomatedRenortsServiceTect nhnch AutomatedRenortsServiceValidateRejectCh AutomatedRenortsServiceValidatio->methodofonscrantAccept File &+X Reject File 4%€SELECT ar.id, ar.uuid, ar.media. type, ar.status, a.typeFROM automated_report_results arJOIN automated_reports a ON a.id = ar.report.idWHERE a.type ='ask_jiminny'LIMIT 10;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 = 68SELECT * FROM automated report results where id = 275;SELECT * FROM automated_repSELECT * FROMont results order by 1d descselect * from activity_searches here user_id = 143;select * fronSELECT 'automated report results'.* FROM 'automated report resultsiautomated remort results', renort id' =automated reoorts'.'1diANDautomated renort results' generated at' IS NOT NUILLI'automated_report_results'. 'sent_at* IS NOT NULLANDAND JSON_CONTAINS(' automated_reports'. 'recipients', 143, '$."users"')SELECT * EROM automated nenonts whenp id = 47.SFLECT * SPOM automated nenonts whene id = 12.SELECT * FROM users WHERE id = 143; # group 28select * from crm_configurations where id = 500:select * from users where name = 'Integration Account': # 1695SELECT * FROM social accounts WHERE sociable id = 1695:select * from activities where crm_configuration id = 39and recording state = 'recorded' and duration > 60and status = 'completed' and actual start time >= '2025-12-01':SELECT * FROM activities WHERE uuid to bin(•458cf915-6914-4000-b083-568763262956') = uuidiselect * from leads›SELECT * FROM activities WHERE uuid to bin('{43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid: # 422003SELECT * FROM activities WHERE id IN (16,422003);SSIEeT * SROM activitios whenp statuc =Socket fail to connect to host:address=(host=localhost) (port=3306)(tvpe=primarv). Connection refusedectc /I View null reauect (todav 12:17)suppont Dally • In 31m100% 5• Tue 19 May 14:29:59AskAnythingPromptServiceTest v+0 ..& console [PROD]# console [euyCascadeAsk Jiminny Report MSo jiminny vSprompt→>getcontent(),Stardet021 A1 A18 V2 V6 A VCownerlluid.ssndreuroups,Sprompt→>getHasReports),That's correct and already there. Thanks to the loadCount in the repository, this is now O(1) extra query for the whole list.What ahout edit/2edito currently doesn't set hasReports, which is fine and intentional:• The newly created prompt (in the isDefaultPrompt branch) is fresh - by definition zero reports.• The edited nromnt mav or mav not have renorts attached. but. editina the nromnt doesn't add or remove renortdoesn't need to recompute ilIf vou want the response to be consistent with create which I changed to explicitly send false ), vou have two options:1. Leave edito as-is— has reports is simolv absent from the response. Cleanest semanticallv: "we didn't touch thisdon't trust whatever you find here."2. Set hasReports: SnewPrompt->qetHasReportso -talls back to an exists query since loadCount wasn't run onthe single prompt). One extra cheap query per edit. Keeps the API response shape uniform with the list endpoint.return new AskAnvthinaPromotdtolSnewPronnt_snetlluidtSnewPrompt->getlargetonCcha rellcerclimidsMv recommendation: leave edit alone. The frontend alreadv has the value from the list and ckinnina it avoids anunnecessary DB hit on every edit. The create() case aot false onlv because a brand-new oromot is trivially known tonave no repons — no query cost.nl .4 tiles with changesvnent/AskAnything/© AskAnythingPromptServiceTest.php +25-2annlComnonont/AckAnvtbina/M.AckAnvthinaDromntSorvico.nhnt7.ies/ D AskAnythingRepository.php +7 -/M AskAnvthinaPromot.ohn +4Reiect alllAccent alliAsk anvthina (884-L)<> Code Claude Qnus 4.7 MediumWN Windsurf Teams66-47UTE.8io 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58044
|
2042
|
14
|
2026-05-19T11:30:02.799212+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190202799_m1.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingPromptServiceTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV (docker)APP₴82APP (-zslreturn new JsonResponse(['error'$error],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLaterMore>0.(ahlSupport Daily • in 30 m100% <78• Tue 19 May 14:30:02ED→Describe what you are looking forJiminny ...scnicre# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Nikolay Yankov6 0• MessagesAdd canvas+. Direct messages8. Nikolay Yankov% Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomove Todor Stamatov "Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Newl:: AppsS Jira CloudToast1Message Nikolay Yankov...
|
NULL
|
2627480494775045757
|
NULL
|
click
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV (docker)APP₴82APP (-zslreturn new JsonResponse(['error'$error],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLaterMore>0.(ahlSupport Daily • in 30 m100% <78• Tue 19 May 14:30:02ED→Describe what you are looking forJiminny ...scnicre# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Nikolay Yankov6 0• MessagesAdd canvas+. Direct messages8. Nikolay Yankov% Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomove Todor Stamatov "Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Newl:: AppsS Jira CloudToast1Message Nikolay Yankov...
|
58042
|
NULL
|
NULL
|
NULL
|
|
58045
|
2043
|
15
|
2026-05-19T11:30:05.939835+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190205939_m2.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingPromptServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Navigateg9 JY-20676-delete-rCodeDAiCallScoringv D Navigateg9 JY-20676-delete-rCodeDAiCallScoringv D AskAnything© AsKAnyningPromptservicelest.onoC HistoryServiceTest.phpDAskJiminnyAil_ billino ManacementCoachingFeedback• CountryDatadogDESD KioskKeractorAskAnytingPromptserwice.ongC ASKJiminnyke!class ASKAnyth1ngPromptServ1celest extends Testcasepublic function testGetAskAnythingPrompts(): void'getHasReports')~ DSCIM> M SlackWaveformMWohhonkcux Rgpct Fle oжe) +E custom.logA console [STAGING]E laravel.log4 SF jiminny@localhost] xC) CoachinaFeedhackCoachl.Icerin.nhnA HS_Jocal [jiminny@localhost]ingPromptServiceTest.php x] © Search.phpA6V1^ v 182186SELECT ar.ig, ar.wid, ar.nedidatype, ar.status, a.tYRgFROM automated_report_results arJOIN automated_reports a ON a.id = ar.cepert.idWHERE a.type = 'ask_jiminny'LIMIT 10; -213215216217218219220-221222223SELECT * FROM automated_reports where id = 71;SELECT * FROM automated_report_results where report. id = 71;UPDATE automated_reports set playbook categories = NULL where ig = 68;SELECT * FROM automated_report_results where jd = 275;SELECT * FROM automated_reports order by id desc;SELECT * FROMont results order by 1d descselect * from activity_searches where user_id = 143;select * fronSELECT"automated_report_results*.* FROM 'automated_report_results'INNER JOIN automated_reportsautomated_report_results'.'report_id' = 'automated_reports'."idANDautomated_report_results'.'generated_at' IS NOT NULL"automated_report_results'. 'sent_at' IS NOT NULLautomated_reports'.'team_id' =1AND JSON_CONTAINS('automated_reports' 'recipients', 143, '$. "users"')SELECT * FROM automated_reports where id = 67;SELECT * FROM automated_reports where id = 42;SELECT * FROM users WHERE id = 143; # group 28select * from teams where id = 3143;select * from crm_configurations where id = 500;select * from users where name = 'Integration Account'; # 1695SELECT * FROM social_accounts WHERE sociable_id = 1695;select * from activities where crm_configuration_id = 39and recording_state = 'recorded' and duration › 60and status = 'completed' and actual_start_time >= '2025-12-01';SELECT * FROM activities WHERE uvid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;select * from leads›SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003SELECT * FROM activities WHERE id IN (16,422003);SELECT * FROM activities where status =Socket fail to connect to host:address=(host=localhost) (port=3306)(type=primary). Connection refused$0lohl"suppont Dally • In som100% 5&• Tue 19 May 14:30:05U AskAnythingPromptServiceTest ~A console [PROD]# console [euyCascadeAsk Jiminny Report M+0 ..fajiminny021 A1 A18 V2 Y6 л VSprompt→>getcontent(),StardetSownerUuid,ssndreuroups,Sprompt→>getHasReports),That's correct and already there. Thanks to the LoadCount in the repository, this is now O(1) extra query for the whole list.What about edit()?edito currently doesn't set hasReports, which is fine and intentional:• The newly created prompt (in the isDefaultPrompt branch) is fresh — by definition zero reports.• The edited nromnt mav or mav not have renorts attached. but. editina the nromnt doesn't add or remove renortassociations, so the frontend already knows the value from when the prompt was loaded in the list. The PUT responsedoesn't need to recompute ilIf vou want the response to be consistent with create which I changed to explicitly send false ), vou have two options:1. Leave edito as-is— has reports is simolv absent from the response. Cleanest semanticallv: "we didn't touch thisdon't trust whatever you find here."2. Set hasReports: SnewPrompt->qetHasReportso -talls back to an exists query since loadCount wasn't run onthe single prompt). One extra cheap query per edit. Keeps the API response shape uniform with the list endpoint.return new AskAnvthinaPromotdtolSnewPrompt->getUuid(),SnewPrompt->getlargetonSshareUsersUuids,Mv recommendation: leave edit alone. The frontend alreadv has the value from the list and ckinnina it avoids anunnecessary DB hit on every edit. The create() case got false only because a brand-new prompt is trivially known tonave no repons — no query cost.nl .4 tiles with changesvnent/AskAnything/ AskAnythingPromptServiceTest.php +22-2annlComnonont/AckAnvtbina/M.AckAnvthinaDromntGorvico.nhnt7ies/ AskAnythingRepository.php +7 -1/M AskAnvthinaPromot.ohn +4Ask anvthing 884-D)<> Code Claude Qnus 4.7 Mediumbjects // View pull request (today 13:17)Reiect allAccent allWN Windsurf Teamc104-1UTF-8f?4 spaces...
|
NULL
|
7515319427480961853
|
NULL
|
click
|
ocr
|
NULL
|
Navigateg9 JY-20676-delete-rCodeDAiCallScoringv D Navigateg9 JY-20676-delete-rCodeDAiCallScoringv D AskAnything© AsKAnyningPromptservicelest.onoC HistoryServiceTest.phpDAskJiminnyAil_ billino ManacementCoachingFeedback• CountryDatadogDESD KioskKeractorAskAnytingPromptserwice.ongC ASKJiminnyke!class ASKAnyth1ngPromptServ1celest extends Testcasepublic function testGetAskAnythingPrompts(): void'getHasReports')~ DSCIM> M SlackWaveformMWohhonkcux Rgpct Fle oжe) +E custom.logA console [STAGING]E laravel.log4 SF jiminny@localhost] xC) CoachinaFeedhackCoachl.Icerin.nhnA HS_Jocal [jiminny@localhost]ingPromptServiceTest.php x] © Search.phpA6V1^ v 182186SELECT ar.ig, ar.wid, ar.nedidatype, ar.status, a.tYRgFROM automated_report_results arJOIN automated_reports a ON a.id = ar.cepert.idWHERE a.type = 'ask_jiminny'LIMIT 10; -213215216217218219220-221222223SELECT * FROM automated_reports where id = 71;SELECT * FROM automated_report_results where report. id = 71;UPDATE automated_reports set playbook categories = NULL where ig = 68;SELECT * FROM automated_report_results where jd = 275;SELECT * FROM automated_reports order by id desc;SELECT * FROMont results order by 1d descselect * from activity_searches where user_id = 143;select * fronSELECT"automated_report_results*.* FROM 'automated_report_results'INNER JOIN automated_reportsautomated_report_results'.'report_id' = 'automated_reports'."idANDautomated_report_results'.'generated_at' IS NOT NULL"automated_report_results'. 'sent_at' IS NOT NULLautomated_reports'.'team_id' =1AND JSON_CONTAINS('automated_reports' 'recipients', 143, '$. "users"')SELECT * FROM automated_reports where id = 67;SELECT * FROM automated_reports where id = 42;SELECT * FROM users WHERE id = 143; # group 28select * from teams where id = 3143;select * from crm_configurations where id = 500;select * from users where name = 'Integration Account'; # 1695SELECT * FROM social_accounts WHERE sociable_id = 1695;select * from activities where crm_configuration_id = 39and recording_state = 'recorded' and duration › 60and status = 'completed' and actual_start_time >= '2025-12-01';SELECT * FROM activities WHERE uvid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;select * from leads›SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003SELECT * FROM activities WHERE id IN (16,422003);SELECT * FROM activities where status =Socket fail to connect to host:address=(host=localhost) (port=3306)(type=primary). Connection refused$0lohl"suppont Dally • In som100% 5&• Tue 19 May 14:30:05U AskAnythingPromptServiceTest ~A console [PROD]# console [euyCascadeAsk Jiminny Report M+0 ..fajiminny021 A1 A18 V2 Y6 л VSprompt→>getcontent(),StardetSownerUuid,ssndreuroups,Sprompt→>getHasReports),That's correct and already there. Thanks to the LoadCount in the repository, this is now O(1) extra query for the whole list.What about edit()?edito currently doesn't set hasReports, which is fine and intentional:• The newly created prompt (in the isDefaultPrompt branch) is fresh — by definition zero reports.• The edited nromnt mav or mav not have renorts attached. but. editina the nromnt doesn't add or remove renortassociations, so the frontend already knows the value from when the prompt was loaded in the list. The PUT responsedoesn't need to recompute ilIf vou want the response to be consistent with create which I changed to explicitly send false ), vou have two options:1. Leave edito as-is— has reports is simolv absent from the response. Cleanest semanticallv: "we didn't touch thisdon't trust whatever you find here."2. Set hasReports: SnewPrompt->qetHasReportso -talls back to an exists query since loadCount wasn't run onthe single prompt). One extra cheap query per edit. Keeps the API response shape uniform with the list endpoint.return new AskAnvthinaPromotdtolSnewPrompt->getUuid(),SnewPrompt->getlargetonSshareUsersUuids,Mv recommendation: leave edit alone. The frontend alreadv has the value from the list and ckinnina it avoids anunnecessary DB hit on every edit. The create() case got false only because a brand-new prompt is trivially known tonave no repons — no query cost.nl .4 tiles with changesvnent/AskAnything/ AskAnythingPromptServiceTest.php +22-2annlComnonont/AckAnvtbina/M.AckAnvthinaDromntGorvico.nhnt7ies/ AskAnythingRepository.php +7 -1/M AskAnvthinaPromot.ohn +4Ask anvthing 884-D)<> Code Claude Qnus 4.7 Mediumbjects // View pull request (today 13:17)Reiect allAccent allWN Windsurf Teamc104-1UTF-8f?4 spaces...
|
58043
|
NULL
|
NULL
|
NULL
|
|
58046
|
2042
|
15
|
2026-05-19T11:30:09.417695+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190209417_m1.jpg...
|
Firefox
|
NULL
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV (docker)APP₴82APP (-zslreturn new JsonResponse(['error'$error],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLaterMore>0.(ahlSupport Daily • in 30 m100% <78• Tue 19 May 14:30:08ED→Describe what you are looking forJiminny ...scnicre# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Nikolay Yankov6 0• MessagesAdd canvas+. Direct messages8. Nikolay Yankov% Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomove Todor Stamatov "Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Newl:: AppsS Jira CloudToast1Message Nikolay Yankov...
|
NULL
|
1520280608532927713
|
NULL
|
click
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV (docker)APP₴82APP (-zslreturn new JsonResponse(['error'$error],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLaterMore>0.(ahlSupport Daily • in 30 m100% <78• Tue 19 May 14:30:08ED→Describe what you are looking forJiminny ...scnicre# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Nikolay Yankov6 0• MessagesAdd canvas+. Direct messages8. Nikolay Yankov% Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomove Todor Stamatov "Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Newl:: AppsS Jira CloudToast1Message Nikolay Yankov...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58047
|
2043
|
16
|
2026-05-19T11:30:10.951851+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190210951_m2.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingPrompt.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormFV faVsco.js~VIewINavicareCode%9 JY-20676- PhostormFV faVsco.js~VIewINavicareCode%9 JY-20676-delete-report-related-objectsKeractorTOOISWindowProject> D AiCallScoringv D AskAnything® ActivityController.php©AskAnythingPromptService.phpAskAnythingController.php©ASKAnythingPromptDto.png© AsKAnytningPromptservicelest.ono© HistoryServiceTest.php>@ AskJiminnyAi© AskJiminnyReportsController.phg© AskAnythingPromptServiceTest.php• _ billino ManacementCoachingFeedbackcountrya customerAplDatadodD DateTimeDealinsiahtsDealRisksEasticSearchEncoding• EncryptionDESn Feature flaasD FFMpegOKeyPoints• KioskD LanguageDetectionMediaPipelineTM MeetinaRotMobileSettingsNotificationDNudgeParagraphBreakerParticipantSpeechD PartitionedCookie> D PlaybackPagePlaylist> 0 ProphetProphetAlvDselMv Mutatorsv M Attributesv → UsenC) ActiveAttrilest.ono@ ScimProvisioninaTest.ohp> M SettinasD Sidekick> M SlackTeaminsightsTM TranscrintionTM TranscrintionSummanM TwilioUploaderm l itility> D Waveform• MWohhonke© AutomatedReportsService.phpk?phpdeclare(strict types=1):namesoace Jaminnv rodels AskAnvthino:› use ...class AskAnythingPrompt extends Modeluse RequiresUUID;protected $table = 'ask_anything_prompts';protected $fillable = ['content'target'protected svislble =protected function castso: arrav....,public function ownero: Relations. Belonasto....?public function getOwner(): ?Userf...no usagesnublic function userPromntso• Relations HasManv....nublic function automatedRenontso• Relations HasManv-...?public function getUvid(): stringf...,public function getId(): intf...,public function getOwnerId(): ?intf.,Dublic function getTargeto: Ă• Accept File 3.J X Reiect File o4ca + 1 of 3 fles →© Search.phpE custom.logA console [STAGING]E laravel.logA HS_Jocal [jiminny@localhost]© CoachingFeedbackCoachUserln.php183SELECT ar.ig, ar.wid, ar.nedidatype, ar.status, a.tYRgFROM automated_report_results arJOIN automated_reports a ON a.ig = ar.cepert.idWHERE a.type = 'ask_jiminny'LIMIT 10; SELECT * FROM automated_reports where id = 71;SELECT * FROM automated_report_results where report.id = 71;UPDATE automated_reports set playbook categories = NULL where ig = 68;SELECT * FROM automated_report_results where jd = 275;SELECT * FROM:order by id desc;where user_id = 143;select * fromSELECTautomated_report_results'.'report_id' = 'automated_reports'."idANDautomated_report_results'.'generated_at' IS NOT NULL"sent_at' IS NOT NULLAND JSON_CONTAINS(' automated_reports'. recipients', 143, 'S. "users'"')SELECT * FROM automated_reports where id = 67;SELECT * FROM automated_reports where id = 42:SELECT * FROM users WHERE id = 143; # group 28select * from crm_configurations where id = 500;select * from users where name = 'Integration Account'; # 1695SELECT * FROM social_accounts WHERE sociable_id = 1695;select * from activities where crm_configuration_id = 39and recording_state = 'recorded' and duration › 60and status = 'completed' and actual_start_time >= ^2025-12-01';220221222223SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;select * from leads;225SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003SELECT * FROM activities WHERE id IN (16,422003);SELECT * FROMSocket fail to connect to host:address=(host=localhost) (port=3306)(type=primary). Connection refused$0lohl"suppont Dally • In som100% 5&• Tue 19 May 14:30:10U AskAnythingPromptServiceTest ~A console (PROD]# console [eu)CascadeAsk Jiminny Report M+0 ...fajiminnySpromot->oetTitleo.021 A1 A18 N2 K6 ^sowneruuzd,SshareusersSshareGroupsSprompt->getHasReports(),That's correct and already there. Thanks to the loadCount in the repository, this is now O(1) extra query for the whole list.What about edit()?edit currently doesn'tset hasReports, which is tine and intentional:• The newly created prompt in the 1sdetaultPrompt branch) is tresh — by detinition zero reports.• The edited prompt may or may not have reports attached, but: editing the prompt doesn't add or remove reportassociations, so the trontend already knows the value from when the prompt was loaded in the list. The Put responsedoesn't need to recompute it.it you want the response to be consistent with create which I changed to explicitly send false, vou have two options1. Leave edit() as-is — has reports is simply absent from the response. Cleanest semantically: "we didn't touch thisdon't trust whatever you find here."2. Set hasReports: $newPrompt->getHasReports() — falls back to an exists() query (since loadCount wasn't run onthe single prompt). One extra cheap query per edit. Keeps the API response shape uniform with the list endpoint.It you want option 2, here's the change1AskAnvthinaPromotService.oho:152-161return new ASKAnvthingPromotdtoSnewPrompt→>getiitlel),SnewPromot->aetContentorSnewPrompt->getTarget(),SshareGroupsuuids,My recommendation: leave edit alone. The frontend already has the value from the list, and skipping it avoids anunnecessary D3 hit on every edit. The createo case aot talse onlv because a brand-new promot is triviallv known tohave no reports - no query cost.ann/Modole/AckAnvthina/MAckAnvthinaDromnt.nbntdAnything/[ AskAnythingPromptService.php +7 -2s/M AskAnvthinaRenositorv.ohnlAsk anvthina (884-L)<> Code Claude Qnus 4.7 Mediumtenll "View all* Reiect alliiAccent allWN Windsurf Teamc104:1UTF-8f?4 spaces...
|
NULL
|
1711997533276366569
|
NULL
|
click
|
ocr
|
NULL
|
PhostormFV faVsco.js~VIewINavicareCode%9 JY-20676- PhostormFV faVsco.js~VIewINavicareCode%9 JY-20676-delete-report-related-objectsKeractorTOOISWindowProject> D AiCallScoringv D AskAnything® ActivityController.php©AskAnythingPromptService.phpAskAnythingController.php©ASKAnythingPromptDto.png© AsKAnytningPromptservicelest.ono© HistoryServiceTest.php>@ AskJiminnyAi© AskJiminnyReportsController.phg© AskAnythingPromptServiceTest.php• _ billino ManacementCoachingFeedbackcountrya customerAplDatadodD DateTimeDealinsiahtsDealRisksEasticSearchEncoding• EncryptionDESn Feature flaasD FFMpegOKeyPoints• KioskD LanguageDetectionMediaPipelineTM MeetinaRotMobileSettingsNotificationDNudgeParagraphBreakerParticipantSpeechD PartitionedCookie> D PlaybackPagePlaylist> 0 ProphetProphetAlvDselMv Mutatorsv M Attributesv → UsenC) ActiveAttrilest.ono@ ScimProvisioninaTest.ohp> M SettinasD Sidekick> M SlackTeaminsightsTM TranscrintionTM TranscrintionSummanM TwilioUploaderm l itility> D Waveform• MWohhonke© AutomatedReportsService.phpk?phpdeclare(strict types=1):namesoace Jaminnv rodels AskAnvthino:› use ...class AskAnythingPrompt extends Modeluse RequiresUUID;protected $table = 'ask_anything_prompts';protected $fillable = ['content'target'protected svislble =protected function castso: arrav....,public function ownero: Relations. Belonasto....?public function getOwner(): ?Userf...no usagesnublic function userPromntso• Relations HasManv....nublic function automatedRenontso• Relations HasManv-...?public function getUvid(): stringf...,public function getId(): intf...,public function getOwnerId(): ?intf.,Dublic function getTargeto: Ă• Accept File 3.J X Reiect File o4ca + 1 of 3 fles →© Search.phpE custom.logA console [STAGING]E laravel.logA HS_Jocal [jiminny@localhost]© CoachingFeedbackCoachUserln.php183SELECT ar.ig, ar.wid, ar.nedidatype, ar.status, a.tYRgFROM automated_report_results arJOIN automated_reports a ON a.ig = ar.cepert.idWHERE a.type = 'ask_jiminny'LIMIT 10; SELECT * FROM automated_reports where id = 71;SELECT * FROM automated_report_results where report.id = 71;UPDATE automated_reports set playbook categories = NULL where ig = 68;SELECT * FROM automated_report_results where jd = 275;SELECT * FROM:order by id desc;where user_id = 143;select * fromSELECTautomated_report_results'.'report_id' = 'automated_reports'."idANDautomated_report_results'.'generated_at' IS NOT NULL"sent_at' IS NOT NULLAND JSON_CONTAINS(' automated_reports'. recipients', 143, 'S. "users'"')SELECT * FROM automated_reports where id = 67;SELECT * FROM automated_reports where id = 42:SELECT * FROM users WHERE id = 143; # group 28select * from crm_configurations where id = 500;select * from users where name = 'Integration Account'; # 1695SELECT * FROM social_accounts WHERE sociable_id = 1695;select * from activities where crm_configuration_id = 39and recording_state = 'recorded' and duration › 60and status = 'completed' and actual_start_time >= ^2025-12-01';220221222223SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;select * from leads;225SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003SELECT * FROM activities WHERE id IN (16,422003);SELECT * FROMSocket fail to connect to host:address=(host=localhost) (port=3306)(type=primary). Connection refused$0lohl"suppont Dally • In som100% 5&• Tue 19 May 14:30:10U AskAnythingPromptServiceTest ~A console (PROD]# console [eu)CascadeAsk Jiminny Report M+0 ...fajiminnySpromot->oetTitleo.021 A1 A18 N2 K6 ^sowneruuzd,SshareusersSshareGroupsSprompt->getHasReports(),That's correct and already there. Thanks to the loadCount in the repository, this is now O(1) extra query for the whole list.What about edit()?edit currently doesn'tset hasReports, which is tine and intentional:• The newly created prompt in the 1sdetaultPrompt branch) is tresh — by detinition zero reports.• The edited prompt may or may not have reports attached, but: editing the prompt doesn't add or remove reportassociations, so the trontend already knows the value from when the prompt was loaded in the list. The Put responsedoesn't need to recompute it.it you want the response to be consistent with create which I changed to explicitly send false, vou have two options1. Leave edit() as-is — has reports is simply absent from the response. Cleanest semantically: "we didn't touch thisdon't trust whatever you find here."2. Set hasReports: $newPrompt->getHasReports() — falls back to an exists() query (since loadCount wasn't run onthe single prompt). One extra cheap query per edit. Keeps the API response shape uniform with the list endpoint.It you want option 2, here's the change1AskAnvthinaPromotService.oho:152-161return new ASKAnvthingPromotdtoSnewPrompt→>getiitlel),SnewPromot->aetContentorSnewPrompt->getTarget(),SshareGroupsuuids,My recommendation: leave edit alone. The frontend already has the value from the list, and skipping it avoids anunnecessary D3 hit on every edit. The createo case aot talse onlv because a brand-new promot is triviallv known tohave no reports - no query cost.ann/Modole/AckAnvthina/MAckAnvthinaDromnt.nbntdAnything/[ AskAnythingPromptService.php +7 -2s/M AskAnvthinaRenositorv.ohnlAsk anvthina (884-L)<> Code Claude Qnus 4.7 Mediumtenll "View all* Reiect alliiAccent allWN Windsurf Teamc104:1UTF-8f?4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58048
|
2042
|
16
|
2026-05-19T11:30:13.355720+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190213355_m1.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingPrompt.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV (docker)APP₴82APP (-zslreturn new JsonResponse(['error'$error],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLaterMore>0.(ahlSupport Daily • in 30 m100% <78• Tue 19 May 14:30:12ED→Describe what you are looking forJiminny ...scnicre# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Nikolay Yankov6 0• MessagesAdd canvas+. Direct messages8. Nikolay Yankov% Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomove Todor Stamatov "Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Newl:: AppsS Jira CloudToast1Message Nikolay Yankov...
|
NULL
|
7699420562659599593
|
NULL
|
click
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV (docker)APP₴82APP (-zslreturn new JsonResponse(['error'$error],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLaterMore>0.(ahlSupport Daily • in 30 m100% <78• Tue 19 May 14:30:12ED→Describe what you are looking forJiminny ...scnicre# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Nikolay Yankov6 0• MessagesAdd canvas+. Direct messages8. Nikolay Yankov% Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomove Todor Stamatov "Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Newl:: AppsS Jira CloudToast1Message Nikolay Yankov...
|
58046
|
NULL
|
NULL
|
NULL
|
|
58049
|
2043
|
17
|
2026-05-19T11:30:16.548906+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190216548_m2.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingPromptService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.10405585,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects<br/>Some incoming commits are not fetched<br/>","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.8194814,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-6966022010367698874
|
-564708849257816637
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
PhostormVIewINavicarecodeFV faVsco.js°9 JY-20676-delete-rProiect vD Ai• @ AskAnythinge AsKAnytningPromptlarget.onpc useraskanythingrrompt.onow calendaD Connectionw contracts0 Crmc) BusinessProcess.ohpc) contiquration.phpc) ContactRole.ohpc) Fieldono© FieldData.phpC) FieldValue.ohoC) Lavout oho© LavoutEntitv.ohr© Log.php© Profile.php© RecordType.php© SyncBatch.php1M GlasticSearchh Foature_ UpporunityD ParticipantD PlaybackThemeD Playlist0 Webhookc) Account.pho© Activity.php© Address.php(c) AlPrompt.php(c) Automated ReportResult.ono(C) Callimoort.oho@ CoachingFeedback.php(C) CoachinaFeedbackVisiollitv.ohoC) CoachinaSection.oho(C) CoachingSectioncriterion.ohv@ CoachinaSectionGriterionFeedback.nho(C) CoachinaSectionFeedback nhn(@. CommentAbstract nhn1Commentinterface.nhn© Contact.php© Device.php© EmailMessage.php© GenericAiPrompt.php@ Group.phpe Inhox.ohoTOOISWindowCActivityController.ongphp api.php© AskAnythingController.php© AskAnythingPromptService.php Xch AutomatedReportsServicelest.onp© AskAnythingPromptDto.php© AskJiminnyReportsController.phg© AutomatedReportsService.php©) AskAnythingPromptServiceTest.phpk?phpdeclare(strict types=1):namesoace Jaminnv comoonent Askanvthino?› use ...class AskAnythingPromptService&BR8public function __construct(private readonly AskAnythingRepository $askAnythingRepository.pravale reduonty userneposttory puser neposttoryrprivate readonly GroupRepositoryInterface $groupRepository.4...}* @return array<AskAnythingPromptDto>public function get(User Suser. AskAnythingPromptTarget Starqet): array$prompts = Sthis->askAnythingRepository->findPromptsByUserAndTarget(stargetsoromotitos =foreach (Sprompts as Spromot) {if (soromot->aetOwnero == nulb <SownerUuid = Soromot->aetowner@->cetluido:SshareGrouns = nwu.// Provide users and groups only if owner of the prompt is current userif ($prompt->getOwnerId() === $user->getId()) {Scharelisens. SchaneGrounsl = Sthis->aetReceiverlluidsSnromnt)•$promptDtos[] = new AskAnythingPromptDto(Snnomnt-Sao+lluid?$prompt->qetTitleO.Snnomnt->ao+Conton+^)starget.$ownerUvid,Accept File &+X Reject File 0*<© Search.phpabiects /I View null reauect (todav 12:17)=custom.log= laravel.log4 SF jiminny@localhost] x4 HS_local [jiminny@localhost]A console [STAGING] © CoachingFeedbackCoachUserln.phpTx: AutovSELECT ar.id, ar.uuid, ar.mediatype, ar.status, a.typeFROM automated_report_results arJOIN automated_reports a ON a.id = ar.reportidWHERE a.type = 'ask_jiminny'LIMIT 10;SELECT * FROM automated_reports where id = 71:SELECT * FROM automated_reresults where report id = 71:UPDATE automated reports set playbook categories = NULL where id = 68stlell * rkuM aucomaced.rport results where 10 = 215SELECT * FROM autorated_reports order by za desc,SELECT * FROMautomated_reportresults order by 1d desc:select * from activity_searches here user_id = 143;select * fromSELECTautomated_renort_results.*FROMautomated_revort_resultsiautomated remort results', renort id' =automated reoorts'.'1diANDautomated_report_results.generated_at IS NOT NULLAND automated_report_results.sent_at IS NOT NULLANDIautomated renorts' 'team id = 1AND JSON_CONTAINS(' automated_reports'. 'recipients', 143, '$."users"')SELECT * FROM automated_reports where id = 67:SELECT * FROM automated_reports where id = 42;SELECT * FROM users WHERE id = 143; # group 28select * from teams where id = 3143;select * from crm_configurations where id = 500:select * from users where name = 'Integration Account': # 1695SELECT * FROM social accounts WHERE sociable id = 1695:select * from activities where crm_configuration id = 39and recording state = 'recorded' and duration › 60and status = 'completed' and actual start time >= '2025-12-01':SELECT * FROM activities WHERE uuid to bin('458cf915-6914-4000-b083-568763262956') = uuid:select * from leads›SELECT * FROM activities WHERE uuid to bin('{43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid: # 422003SELECT * FROM activities WHERE id IN (16,422003);SELECT * FROM activities where status = 'failed':Socket fail to connect to host:address=(host=localhost) (port=3306)(tvpe=primarv). Connection refusedhell"suppont Dally • In som100% 5• Tue 19 May 14:30:16AskAnythingPromptServiceTest& console [PROD]# console [euyCascadeAsk Jiminny Report M+0 ..So jiminny vSprompt→>getuuid),021 A1 A18 V2 V6 ^>prompt->getcontento.sprompt-gechaskeportstThat's correct and already there. Thanks to the loadCount in the revositorv. this is now Ol1) extra auery for the whole list.What about edit o?edit currenty doesn uset hasReports, which is tine and intentional:• The newly created prompt (in the isDefaultPrompt branch) is fresh - by definition zero reports.• The edited promot may or may not have reports attached, but: editina the prompt doesn't add or remove report)doesn't need to recomoute itIf you want the response to be consistent with create() (which | changed to explicitly send false), you have two options:1. Leave edit() as-is — has reports is simply absent from the response. Cleanest semantically: "we didn't touch thisdon't trust whatever vou find here"2. Set hasReports: $newPrompt->getHasReports() — falls back to an exists() query (since loadCount wasn't run onthe sinale promot). One extra chean query ver edit. Keens the APl response shave unitorm with the list endooint.Iyou want option &, nere's the change-AskAnythingPro.AntCorindnhniieoreturn new AskAnythingPromptDto(SnewPromnt_snetTitle/).SnewPrompt->getTarget(),Suser->getUuidonSshareUsersUuids$shareGroupsUuids,My recommendation: leave edit alone. The trontend already has the value from the list. and skionina it avoids anunnecessary DB hit on every edit. The create() case got false only because a brand-new prompt is trivially known tohave no reports — no query cost.hina/MAskAnvthinaPromntService.ohn t7ies/M AskAnvthinaRenositorv.ohnAsk anvthina (&4-L)<> Code Claude Qnus 1.7 Medium* Reiect alliiAccent alliW Windsurf Team1-1.UTE.8Po 4 spaces...
|
58047
|
NULL
|
NULL
|
NULL
|
|
58050
|
2042
|
17
|
2026-05-19T11:30:18.885113+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190218885_m1.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingPromptService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV (docker)APP₴82APP (-zslreturn new JsonResponse(['error'$error],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLaterMore>0.(ahlSupport Daily • in 30 m100% <7Tue 19 May 14:30:18ED→Describe what you are looking forJiminny ...scnicre# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Nikolay Yankov6 0• MessagesAdd canvas+. Direct messages8. Nikolay Yankov% Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomove Todor Stamatov "Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Newl:: AppsS Jira CloudToast1Message Nikolay Yankov...
|
NULL
|
-722778066163500425
|
NULL
|
click
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV (docker)APP₴82APP (-zslreturn new JsonResponse(['error'$error],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLaterMore>0.(ahlSupport Daily • in 30 m100% <7Tue 19 May 14:30:18ED→Describe what you are looking forJiminny ...scnicre# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Nikolay Yankov6 0• MessagesAdd canvas+. Direct messages8. Nikolay Yankov% Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomove Todor Stamatov "Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Newl:: AppsS Jira CloudToast1Message Nikolay Yankov...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58051
|
2043
|
18
|
2026-05-19T11:30:20.842496+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190220842_m2.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingPromptService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormViewINavicarecodeKeractorTOOISWindowmelpFV PhostormViewINavicarecodeKeractorTOOISWindowmelpFV faVsco.js°9 JY-20676-delete-report-related-objeProiectCActivityController.ongphp api.php© AskAnythingController.php© AskAnythingPromptDto.php© AskAnythingPromptService.php Xch AutomatedReportsServicelest.onp( Events©ASKAnythingPromptDto.png© AsKAnytningPromptservice.ongc Historyservice.ong© AskJiminnyReportsController.phg_ ASKJIminnyAlWAWSu cachew countryDatabase→ DatadogDatettimeDealinsiahtsN DealRisks1N GlasticSearchM EloquentEncoding• Encryption1M Sakenm FeaturerlagsD FFMpegD FileSystemD Gong_ cuzzienutou reyPolntsKIOSK_ LanquageDetectionW LOCKSW Math_MediapioelineM MeetinaBot2 MobileSettinasNudaelIM ParagranhBreake1 PartitionedCookieM PlavbackPadeM PlavlistProphetM PronhetAfM DrosnorWorkdM Auonc© AutomatedReportsService.php©) AskAnythingPromptServiceTest.phpk?phpdeclare(strict types=1):namespace Jaminnv comoonent Askanvthino?› use ...class AskAnythingPromptService&BR8public function __construct(private readonly AskAnythingRepository $askAnythingRepository.private readonly UserRepository $userRepository,private readonly GroupRepositoryInterface $groupRepository.4...* @return array<AskAnythingPromptDto>public function get(User Suser. AskAnythingPromptTarget Starqet): arraySprompts = Sthis->askAnythingRepository->findPromptsByUserAndTarget(stargetspromotitos =foreach (Spromots as Spromot) {if (soromot->aetOwnero == nulb <SownerUuid = Soromot->aet0wnero->cetluido:SshareGrouns = nwuProvide usens and arouns onlv if ownen of the nromnt is current usenif ($prompt->getOwnerId() === $user->getId()) {Ssharelisens. SchaneGrounsi = Sthis->aetReceiverlluids/Snromnt)•$promptDtos[] = new AskAnythingPromptDto(Snnomnt-Sao+lluid?$prompt->getTitleO.Snnomnt-sao+Conton+/)randor=custom.log= laravel.log4 SF jiminny@localhost] x4 HS_local [jiminny@localhost]& console [PROD]# console [euyA console [STAGING]© Search.php181A2J2л v .21.221222—223© CoachingFeedbackCoachUserln.phpTx: AutovSo jiminny v021 A1 A18 V2 V6 ^SELECT ar.id, ar.uuid, ar.mediatype, ar.status, a.typeFROM automated_report_results arJOIN automated_reports a ON a.id = ar.reportidWHERE a.type = 'ask_jiminny'LIMIT 10;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 = 68SELECT * FROM automated report results where id = 275;SELECT * FROM autorrts order by za desc;SELECT * FROMautomated_reportresults order by 1d desc:select * from activity_searches here user_id = 143;select * fromSELECTautomated_report_results.* FROMautomated_revortresultsiINNER JOIN automated_reportsautomated remort results', renort id' =automated_reoorts'.'1dìANDautomated_report_results.generated_at IS NOT NULLAND 'automated_report_results'.'sent_at' IS NOT NULLANDIautomated renorts' 'team id = 1AND JSON_CONTAINS(' automated_reports'. 'recipients', 143, '$."users"')SELECT * FROM automated_reports where id = 67:SELECT * FROM automated_reports where id = 42;SELECT * FROM users WHERE id = 143; # group 28select * from teams where id = 3143;select * from crm_configurations where id = 500:select * from users where name = 'Integration Account': # 1695SELECT * FROM social accounts WHERE sociable id = 1695:select * from activities where crm_configuration id = 39and recording state = 'recorded' and duration > 60and status = 'completed' and actual start time >= '2025-12-01':SELECT * FROM activities WHERE uuid to bin('458cf915-6914-4000-b083-568763262956') = uuid:select * from leads›225SELECT * FROM activities WHERE uuid to bin('{43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid: # 422003SELECT * FROM activities WHERE id IN (16,422003);SELECT * FROM activities where status = 'failed':Socket fail to connect to host:address=(host=localhost) (port=3306)(tvpe=primarv). Connection refused40hell"suppont Dally • In som100% 5• Tue 19 May 14:30:20AskAnythingPromptServiceTestCascadeAsk Jiminny Report M+0 ..SpromptDtosl] = nev• AskAnvthinaPromotDtodSprompt->gecuuzd,Sprompt→>gettitle)$prompt->getContent(),SShareusers,Soromot->aetHasReportsoThat's correct and already there, Thanks to the loadcount in the revository. this is now Oli extra query tor the whole list.What about editoredit() currently doesn't set hasReports, which is fine and intentional:• The newly created prompt (in the isDefaultPrompt branch) is fresh - by definition zero reports.• The edited prompt may or may not have reports attached, but: editing the prompt doesn't add or remove reportassoclations, so the trontend alreacy knows the value Trom when the prompr was loaded in the list. Ine Pul responseIyou want the response to oe consistent with createl wnichi changed to expllcitly send talse , you nave two options1. Leave edit() as-is— has_reports is simply absent from the response. Cleanest semantically: "we didn't touch this;don't trust whatever you find here."2. Set hasReports: $newPrompt->getHasReports() — falls back to an exists() query (since loadCount wasn't run onthe sinale promot). One extra chean query per edit. Keeps the APl response shape uniform with the list endoointIf you want option 2, here's the change-ASkAnythingPnhn-159.161return new AskAnythingPromptDto(SnewPrompt->getcontentSnewPromnt->aetTaraet().shareGrounslluidsMy recommendation: leave edit alone. The frontend already has the value from the list. and skiopina it avoids anunnecessary DB hit on every edit. The create() case got false only because a brand-new prompt is trivially known tohave no reports — no auery cost.sitories/ M AskAnvthinaRenositorv.ohv +7Ask anvthina (&4-L)+ @ Code Claude Qnus 4.7 MediumView all* Reiect alliiAccent alliW Windsurf Team1-1.UTE.8io 4 spaces...
|
NULL
|
655610464290193723
|
NULL
|
visual_change
|
ocr
|
NULL
|
PhostormViewINavicarecodeKeractorTOOISWindowmelpFV PhostormViewINavicarecodeKeractorTOOISWindowmelpFV faVsco.js°9 JY-20676-delete-report-related-objeProiectCActivityController.ongphp api.php© AskAnythingController.php© AskAnythingPromptDto.php© AskAnythingPromptService.php Xch AutomatedReportsServicelest.onp( Events©ASKAnythingPromptDto.png© AsKAnytningPromptservice.ongc Historyservice.ong© AskJiminnyReportsController.phg_ ASKJIminnyAlWAWSu cachew countryDatabase→ DatadogDatettimeDealinsiahtsN DealRisks1N GlasticSearchM EloquentEncoding• Encryption1M Sakenm FeaturerlagsD FFMpegD FileSystemD Gong_ cuzzienutou reyPolntsKIOSK_ LanquageDetectionW LOCKSW Math_MediapioelineM MeetinaBot2 MobileSettinasNudaelIM ParagranhBreake1 PartitionedCookieM PlavbackPadeM PlavlistProphetM PronhetAfM DrosnorWorkdM Auonc© AutomatedReportsService.php©) AskAnythingPromptServiceTest.phpk?phpdeclare(strict types=1):namespace Jaminnv comoonent Askanvthino?› use ...class AskAnythingPromptService&BR8public function __construct(private readonly AskAnythingRepository $askAnythingRepository.private readonly UserRepository $userRepository,private readonly GroupRepositoryInterface $groupRepository.4...* @return array<AskAnythingPromptDto>public function get(User Suser. AskAnythingPromptTarget Starqet): arraySprompts = Sthis->askAnythingRepository->findPromptsByUserAndTarget(stargetspromotitos =foreach (Spromots as Spromot) {if (soromot->aetOwnero == nulb <SownerUuid = Soromot->aet0wnero->cetluido:SshareGrouns = nwuProvide usens and arouns onlv if ownen of the nromnt is current usenif ($prompt->getOwnerId() === $user->getId()) {Ssharelisens. SchaneGrounsi = Sthis->aetReceiverlluids/Snromnt)•$promptDtos[] = new AskAnythingPromptDto(Snnomnt-Sao+lluid?$prompt->getTitleO.Snnomnt-sao+Conton+/)randor=custom.log= laravel.log4 SF jiminny@localhost] x4 HS_local [jiminny@localhost]& console [PROD]# console [euyA console [STAGING]© Search.php181A2J2л v .21.221222—223© CoachingFeedbackCoachUserln.phpTx: AutovSo jiminny v021 A1 A18 V2 V6 ^SELECT ar.id, ar.uuid, ar.mediatype, ar.status, a.typeFROM automated_report_results arJOIN automated_reports a ON a.id = ar.reportidWHERE a.type = 'ask_jiminny'LIMIT 10;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 = 68SELECT * FROM automated report results where id = 275;SELECT * FROM autorrts order by za desc;SELECT * FROMautomated_reportresults order by 1d desc:select * from activity_searches here user_id = 143;select * fromSELECTautomated_report_results.* FROMautomated_revortresultsiINNER JOIN automated_reportsautomated remort results', renort id' =automated_reoorts'.'1dìANDautomated_report_results.generated_at IS NOT NULLAND 'automated_report_results'.'sent_at' IS NOT NULLANDIautomated renorts' 'team id = 1AND JSON_CONTAINS(' automated_reports'. 'recipients', 143, '$."users"')SELECT * FROM automated_reports where id = 67:SELECT * FROM automated_reports where id = 42;SELECT * FROM users WHERE id = 143; # group 28select * from teams where id = 3143;select * from crm_configurations where id = 500:select * from users where name = 'Integration Account': # 1695SELECT * FROM social accounts WHERE sociable id = 1695:select * from activities where crm_configuration id = 39and recording state = 'recorded' and duration > 60and status = 'completed' and actual start time >= '2025-12-01':SELECT * FROM activities WHERE uuid to bin('458cf915-6914-4000-b083-568763262956') = uuid:select * from leads›225SELECT * FROM activities WHERE uuid to bin('{43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid: # 422003SELECT * FROM activities WHERE id IN (16,422003);SELECT * FROM activities where status = 'failed':Socket fail to connect to host:address=(host=localhost) (port=3306)(tvpe=primarv). Connection refused40hell"suppont Dally • In som100% 5• Tue 19 May 14:30:20AskAnythingPromptServiceTestCascadeAsk Jiminny Report M+0 ..SpromptDtosl] = nev• AskAnvthinaPromotDtodSprompt->gecuuzd,Sprompt→>gettitle)$prompt->getContent(),SShareusers,Soromot->aetHasReportsoThat's correct and already there, Thanks to the loadcount in the revository. this is now Oli extra query tor the whole list.What about editoredit() currently doesn't set hasReports, which is fine and intentional:• The newly created prompt (in the isDefaultPrompt branch) is fresh - by definition zero reports.• The edited prompt may or may not have reports attached, but: editing the prompt doesn't add or remove reportassoclations, so the trontend alreacy knows the value Trom when the prompr was loaded in the list. Ine Pul responseIyou want the response to oe consistent with createl wnichi changed to expllcitly send talse , you nave two options1. Leave edit() as-is— has_reports is simply absent from the response. Cleanest semantically: "we didn't touch this;don't trust whatever you find here."2. Set hasReports: $newPrompt->getHasReports() — falls back to an exists() query (since loadCount wasn't run onthe sinale promot). One extra chean query per edit. Keeps the APl response shape uniform with the list endoointIf you want option 2, here's the change-ASkAnythingPnhn-159.161return new AskAnythingPromptDto(SnewPrompt->getcontentSnewPromnt->aetTaraet().shareGrounslluidsMy recommendation: leave edit alone. The frontend already has the value from the list. and skiopina it avoids anunnecessary DB hit on every edit. The create() case got false only because a brand-new prompt is trivially known tohave no reports — no auery cost.sitories/ M AskAnvthinaRenositorv.ohv +7Ask anvthina (&4-L)+ @ Code Claude Qnus 4.7 MediumView all* Reiect alliiAccent alliW Windsurf Team1-1.UTE.8io 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58052
|
2042
|
18
|
2026-05-19T11:30:23.028661+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190223028_m1.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingRepository.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV (docker)APP₴82APP (-zslreturn new JsonResponse(['error'$error],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLaterMore>0.(ahlSupport Daily • in 30 m100% <78• Tue 19 May 14:30:22ED→Describe what you are looking forJiminny ...scnicre# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Nikolay Yankov6 0• MessagesAdd canvas+. Direct messages8. Nikolay Yankov% Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomove Todor Stamatov "Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Newl:: AppsS Jira CloudToast1Message Nikolay Yankov...
|
NULL
|
7386130141885383548
|
NULL
|
click
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV (docker)APP₴82APP (-zslreturn new JsonResponse(['error'$error],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLaterMore>0.(ahlSupport Daily • in 30 m100% <78• Tue 19 May 14:30:22ED→Describe what you are looking forJiminny ...scnicre# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Nikolay Yankov6 0• MessagesAdd canvas+. Direct messages8. Nikolay Yankov% Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomove Todor Stamatov "Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Newl:: AppsS Jira CloudToast1Message Nikolay Yankov...
|
58050
|
NULL
|
NULL
|
NULL
|
|
58054
|
2043
|
19
|
2026-05-19T11:30:29.439619+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190229439_m2.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingRepository.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskAnythingPromptServiceTest
Run 'AskAnythingPromptServiceTest'
Debug 'AskAnythingPromptServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AskAnything\UserAskAnythingPrompt;
use Jiminny\Models\Group;
use Jiminny\Models\User;
class AskAnythingRepository
{
/**
* @return Collection<UserAskAnythingPrompt>
*/
public function findSharedUsersAndGroupsByPromptId(int $promptId): Collection
{
return UserAskAnythingPrompt::query()
->where('prompt_id', $promptId)
->where('is_removed', false)
->get();
}
public function findSharedPromptByUser(int $promptId, User $user): ?UserAskAnythingPrompt
{
return UserAskAnythingPrompt::with('prompt')
->where('prompt_id', $promptId)
->where('user_id', $user->getId())
->first();
}
public function findSharedPromptByUserGroup(int $promptId, User $user): ?UserAskAnythingPrompt
{
$userGroupId = $user->getGroupId();
return UserAskAnythingPrompt::with('prompt')
->where('prompt_id', $promptId)
->where(static function ($query) use ($userGroupId): void {
if ($userGroupId !== null) {
$query->where('group_id', $userGroupId);
}
})
->first();
}
/**
* @return Collection<AskAnythingPrompt>
*/
public function findPromptsByUserAndTarget(User $user, AskAnythingPromptTarget $target): Collection
{
$userGroupId = $user->getGroupId();
$usersOwnedPrompts = UserAskAnythingPrompt::with('prompt')
->where(static function ($query) use ($user, $userGroupId): void {
$query
->where('user_id', $user->getId());
if ($userGroupId !== null) {
$query->orWhere('group_id', $userGroupId);
}
})
->where('is_removed', false)
->whereHas('prompt', function (Builder $query) use ($target) {
$query->where('target', $target);
})
->orderByRaw('ISNULL(`order`), `order` ASC, `prompt_id` ASC')
->get()
->map(function (UserAskAnythingPrompt $userPrompt) {
return $userPrompt->getPrompt();
});
// Remove those prompts that are hidden for the current user
$usersOwnedPromptsFiltered = $usersOwnedPrompts->filter(function (AskAnythingPrompt $userPrompt) use ($user) {
$promptId = $userPrompt->getId();
$userDisabledPrompt = UserAskAnythingPrompt::query()
->where('prompt_id', $promptId)
->where('is_removed', true)
->where('user_id', $user->getId())
->first();
return $userDisabledPrompt === null;
});
$defaultNonChangedPrompts = AskAnythingPrompt::where('target', $target)
->whereDoesntHave('userPrompts', function ($query) use ($user) {
$query->where('user_id', $user->getId());
})
->whereNull('owner_id')
->get();
$allPrompts = $defaultNonChangedPrompts->merge($usersOwnedPromptsFiltered);
if ($allPrompts->isNotEmpty()) {
$allPrompts->loadCount('automatedReports');
}
return $allPrompts;
}
/**
* @param array<User> $shareUsers
* @param array<Group> $shareGroups
*/
public function createPrompt(
User $user,
AskAnythingPromptTarget $target,
string $title,
string $content,
array $shareUsers,
array $shareGroups,
): AskAnythingPrompt {
$prompt = AskAnythingPrompt::create([
'title' => $title,
'content' => $content,
'target' => $target,
'owner_id' => $user->getId(),
]);
UserAskAnythingPrompt::create([
'user_id' => $user->getId(),
'prompt_id' => $prompt->getId(),
]);
foreach ($shareUsers as $shareUser) {
UserAskAnythingPrompt::create([
'user_id' => $shareUser->getId(),
'prompt_id' => $prompt->getId(),
]);
}
foreach ($shareGroups as $shareGroup) {
UserAskAnythingPrompt::create([
'group_id' => $shareGroup->getId(),
'prompt_id' => $prompt->getId(),
]);
}
return $prompt;
}
/**
* @param array<User> $shareUsers
* @param array<Group> $shareGroups
*/
public function editPrompt(
AskAnythingPrompt $prompt,
string $title,
string $content,
array $shareUsers,
array $shareGroups,
): AskAnythingPrompt {
$prompt->update([
'title' => $title,
'content' => $content,
]);
$previousUserPrompts = UserAskAnythingPrompt::query()
->where('prompt_id', $prompt->getId())
->whereNull('group_id')
->whereNotNull('user_id')
->whereNot('user_id', $prompt->getOwnerId())
->get();
$previousGroupPrompts = UserAskAnythingPrompt::query()
->where('prompt_id', $prompt->getId())
->whereNotNull('group_id')
->whereNull('user_id')
->get();
$shareUserPrompts = [];
foreach ($shareUsers as $shareUser) {
$shareUserPrompts[] = UserAskAnythingPrompt::create([
'user_id' => $shareUser->getId(),
'prompt_id' => $prompt->getId(),
]);
}
$shareGroupPrompts = [];
foreach ($shareGroups as $shareGroup) {
$shareGroupPrompts[] = UserAskAnythingPrompt::create([
'group_id' => $shareGroup->getId(),
'prompt_id' => $prompt->getId(),
]);
}
// Remove those users that are no longer added
$diffUsers = $previousUserPrompts->diff($shareUserPrompts);
foreach ($diffUsers as $previousUserPrompt) {
$previousUserPrompt->delete();
}
// Remove those groups that are no longer added
$diffGroups = $previousGroupPrompts->diff($shareGroupPrompts);
foreach ($diffGroups as $previousGroupPrompt) {
$previousGroupPrompt->delete();
}
return $prompt;
}
public function deletePrompt(AskAnythingPrompt $prompt): void
{
// Also deletes all associations with users
$prompt->delete();
}
public function hidePromptForUser(AskAnythingPrompt $prompt, User $user): AskAnythingPrompt
{
$userPromptSettings = UserAskAnythingPrompt::where('user_id', $user->getId())
->where('prompt_id', $prompt->getId())
->first();
if ($userPromptSettings === null) {
$userPromptSettings = UserAskAnythingPrompt::create([
'user_id' => $user->getId(),
'prompt_id' => $prompt->getId(),
]);
}
$userPromptSettings->update([
'is_removed' => true,
]);
return $prompt;
}
public function getPromptByUuid(string $uuid): ?AskAnythingPrompt
{
return AskAnythingPrompt::where('uuid', AskAnythingPrompt::toOptimized($uuid))->first();
}
public function orderPromptForUser(AskAnythingPrompt $prompt, User $user, int $order): void
{
$userPromptSettings = UserAskAnythingPrompt::where('user_id', $user->getId())
->where('prompt_id', $prompt->getId())
->first();
if ($userPromptSettings === null) {
$userPromptSettings = UserAskAnythingPrompt::create([
'user_id' => $user->getId(),
'prompt_id' => $prompt->getId(),
]);
}
$userPromptSettings->update([
'order' => $order,
]);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.10405585,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects<br/>Some incoming commits are not fetched<br/>","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.8194814,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskAnythingPromptServiceTest","depth":6,"bounds":{"left":0.83477396,"top":0.019952115,"width":0.080784574,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskAnythingPromptServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskAnythingPromptServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"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},"on_screen":true,"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},"on_screen":true,"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},"on_screen":true,"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},"on_screen":true,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","depth":4,"bounds":{"left":0.4005984,"top":0.15003991,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.4192154,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"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 Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AskAnything\\UserAskAnythingPrompt;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\User;\n\nclass AskAnythingRepository\n{\n /**\n * @return Collection<UserAskAnythingPrompt>\n */\n public function findSharedUsersAndGroupsByPromptId(int $promptId): Collection\n {\n return UserAskAnythingPrompt::query()\n ->where('prompt_id', $promptId)\n ->where('is_removed', false)\n ->get();\n }\n\n public function findSharedPromptByUser(int $promptId, User $user): ?UserAskAnythingPrompt\n {\n return UserAskAnythingPrompt::with('prompt')\n ->where('prompt_id', $promptId)\n ->where('user_id', $user->getId())\n ->first();\n }\n\n public function findSharedPromptByUserGroup(int $promptId, User $user): ?UserAskAnythingPrompt\n {\n $userGroupId = $user->getGroupId();\n\n return UserAskAnythingPrompt::with('prompt')\n ->where('prompt_id', $promptId)\n ->where(static function ($query) use ($userGroupId): void {\n if ($userGroupId !== null) {\n $query->where('group_id', $userGroupId);\n }\n })\n ->first();\n }\n\n /**\n * @return Collection<AskAnythingPrompt>\n */\n public function findPromptsByUserAndTarget(User $user, AskAnythingPromptTarget $target): Collection\n {\n $userGroupId = $user->getGroupId();\n $usersOwnedPrompts = UserAskAnythingPrompt::with('prompt')\n ->where(static function ($query) use ($user, $userGroupId): void {\n $query\n ->where('user_id', $user->getId());\n\n if ($userGroupId !== null) {\n $query->orWhere('group_id', $userGroupId);\n }\n })\n ->where('is_removed', false)\n ->whereHas('prompt', function (Builder $query) use ($target) {\n $query->where('target', $target);\n })\n ->orderByRaw('ISNULL(`order`), `order` ASC, `prompt_id` ASC')\n ->get()\n ->map(function (UserAskAnythingPrompt $userPrompt) {\n return $userPrompt->getPrompt();\n });\n\n // Remove those prompts that are hidden for the current user\n $usersOwnedPromptsFiltered = $usersOwnedPrompts->filter(function (AskAnythingPrompt $userPrompt) use ($user) {\n $promptId = $userPrompt->getId();\n $userDisabledPrompt = UserAskAnythingPrompt::query()\n ->where('prompt_id', $promptId)\n ->where('is_removed', true)\n ->where('user_id', $user->getId())\n ->first();\n\n return $userDisabledPrompt === null;\n });\n\n $defaultNonChangedPrompts = AskAnythingPrompt::where('target', $target)\n ->whereDoesntHave('userPrompts', function ($query) use ($user) {\n $query->where('user_id', $user->getId());\n })\n ->whereNull('owner_id')\n ->get();\n\n $allPrompts = $defaultNonChangedPrompts->merge($usersOwnedPromptsFiltered);\n\n if ($allPrompts->isNotEmpty()) {\n $allPrompts->loadCount('automatedReports');\n }\n\n return $allPrompts;\n }\n\n /**\n * @param array<User> $shareUsers\n * @param array<Group> $shareGroups\n */\n public function createPrompt(\n User $user,\n AskAnythingPromptTarget $target,\n string $title,\n string $content,\n array $shareUsers,\n array $shareGroups,\n ): AskAnythingPrompt {\n $prompt = AskAnythingPrompt::create([\n 'title' => $title,\n 'content' => $content,\n 'target' => $target,\n 'owner_id' => $user->getId(),\n ]);\n\n UserAskAnythingPrompt::create([\n 'user_id' => $user->getId(),\n 'prompt_id' => $prompt->getId(),\n ]);\n\n foreach ($shareUsers as $shareUser) {\n UserAskAnythingPrompt::create([\n 'user_id' => $shareUser->getId(),\n 'prompt_id' => $prompt->getId(),\n ]);\n }\n\n foreach ($shareGroups as $shareGroup) {\n UserAskAnythingPrompt::create([\n 'group_id' => $shareGroup->getId(),\n 'prompt_id' => $prompt->getId(),\n ]);\n }\n\n return $prompt;\n }\n\n /**\n * @param array<User> $shareUsers\n * @param array<Group> $shareGroups\n */\n public function editPrompt(\n AskAnythingPrompt $prompt,\n string $title,\n string $content,\n array $shareUsers,\n array $shareGroups,\n ): AskAnythingPrompt {\n $prompt->update([\n 'title' => $title,\n 'content' => $content,\n ]);\n\n $previousUserPrompts = UserAskAnythingPrompt::query()\n ->where('prompt_id', $prompt->getId())\n ->whereNull('group_id')\n ->whereNotNull('user_id')\n ->whereNot('user_id', $prompt->getOwnerId())\n ->get();\n\n $previousGroupPrompts = UserAskAnythingPrompt::query()\n ->where('prompt_id', $prompt->getId())\n ->whereNotNull('group_id')\n ->whereNull('user_id')\n ->get();\n\n $shareUserPrompts = [];\n foreach ($shareUsers as $shareUser) {\n $shareUserPrompts[] = UserAskAnythingPrompt::create([\n 'user_id' => $shareUser->getId(),\n 'prompt_id' => $prompt->getId(),\n ]);\n }\n\n $shareGroupPrompts = [];\n foreach ($shareGroups as $shareGroup) {\n $shareGroupPrompts[] = UserAskAnythingPrompt::create([\n 'group_id' => $shareGroup->getId(),\n 'prompt_id' => $prompt->getId(),\n ]);\n }\n\n // Remove those users that are no longer added\n $diffUsers = $previousUserPrompts->diff($shareUserPrompts);\n foreach ($diffUsers as $previousUserPrompt) {\n $previousUserPrompt->delete();\n }\n\n // Remove those groups that are no longer added\n $diffGroups = $previousGroupPrompts->diff($shareGroupPrompts);\n foreach ($diffGroups as $previousGroupPrompt) {\n $previousGroupPrompt->delete();\n }\n\n return $prompt;\n }\n\n public function deletePrompt(AskAnythingPrompt $prompt): void\n {\n // Also deletes all associations with users\n $prompt->delete();\n }\n\n public function hidePromptForUser(AskAnythingPrompt $prompt, User $user): AskAnythingPrompt\n {\n $userPromptSettings = UserAskAnythingPrompt::where('user_id', $user->getId())\n ->where('prompt_id', $prompt->getId())\n ->first();\n\n if ($userPromptSettings === null) {\n $userPromptSettings = UserAskAnythingPrompt::create([\n 'user_id' => $user->getId(),\n 'prompt_id' => $prompt->getId(),\n ]);\n }\n\n $userPromptSettings->update([\n 'is_removed' => true,\n ]);\n\n return $prompt;\n }\n\n public function getPromptByUuid(string $uuid): ?AskAnythingPrompt\n {\n return AskAnythingPrompt::where('uuid', AskAnythingPrompt::toOptimized($uuid))->first();\n }\n\n public function orderPromptForUser(AskAnythingPrompt $prompt, User $user, int $order): void\n {\n $userPromptSettings = UserAskAnythingPrompt::where('user_id', $user->getId())\n ->where('prompt_id', $prompt->getId())\n ->first();\n\n if ($userPromptSettings === null) {\n $userPromptSettings = UserAskAnythingPrompt::create([\n 'user_id' => $user->getId(),\n 'prompt_id' => $prompt->getId(),\n ]);\n }\n\n $userPromptSettings->update([\n 'order' => $order,\n ]);\n }\n}","depth":4,"bounds":{"left":0.15990691,"top":0.0,"width":0.32912233,"height":1.0},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AskAnything\\UserAskAnythingPrompt;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\User;\n\nclass AskAnythingRepository\n{\n /**\n * @return Collection<UserAskAnythingPrompt>\n */\n public function findSharedUsersAndGroupsByPromptId(int $promptId): Collection\n {\n return UserAskAnythingPrompt::query()\n ->where('prompt_id', $promptId)\n ->where('is_removed', false)\n ->get();\n }\n\n public function findSharedPromptByUser(int $promptId, User $user): ?UserAskAnythingPrompt\n {\n return UserAskAnythingPrompt::with('prompt')\n ->where('prompt_id', $promptId)\n ->where('user_id', $user->getId())\n ->first();\n }\n\n public function findSharedPromptByUserGroup(int $promptId, User $user): ?UserAskAnythingPrompt\n {\n $userGroupId = $user->getGroupId();\n\n return UserAskAnythingPrompt::with('prompt')\n ->where('prompt_id', $promptId)\n ->where(static function ($query) use ($userGroupId): void {\n if ($userGroupId !== null) {\n $query->where('group_id', $userGroupId);\n }\n })\n ->first();\n }\n\n /**\n * @return Collection<AskAnythingPrompt>\n */\n public function findPromptsByUserAndTarget(User $user, AskAnythingPromptTarget $target): Collection\n {\n $userGroupId = $user->getGroupId();\n $usersOwnedPrompts = UserAskAnythingPrompt::with('prompt')\n ->where(static function ($query) use ($user, $userGroupId): void {\n $query\n ->where('user_id', $user->getId());\n\n if ($userGroupId !== null) {\n $query->orWhere('group_id', $userGroupId);\n }\n })\n ->where('is_removed', false)\n ->whereHas('prompt', function (Builder $query) use ($target) {\n $query->where('target', $target);\n })\n ->orderByRaw('ISNULL(`order`), `order` ASC, `prompt_id` ASC')\n ->get()\n ->map(function (UserAskAnythingPrompt $userPrompt) {\n return $userPrompt->getPrompt();\n });\n\n // Remove those prompts that are hidden for the current user\n $usersOwnedPromptsFiltered = $usersOwnedPrompts->filter(function (AskAnythingPrompt $userPrompt) use ($user) {\n $promptId = $userPrompt->getId();\n $userDisabledPrompt = UserAskAnythingPrompt::query()\n ->where('prompt_id', $promptId)\n ->where('is_removed', true)\n ->where('user_id', $user->getId())\n ->first();\n\n return $userDisabledPrompt === null;\n });\n\n $defaultNonChangedPrompts = AskAnythingPrompt::where('target', $target)\n ->whereDoesntHave('userPrompts', function ($query) use ($user) {\n $query->where('user_id', $user->getId());\n })\n ->whereNull('owner_id')\n ->get();\n\n $allPrompts = $defaultNonChangedPrompts->merge($usersOwnedPromptsFiltered);\n\n if ($allPrompts->isNotEmpty()) {\n $allPrompts->loadCount('automatedReports');\n }\n\n return $allPrompts;\n }\n\n /**\n * @param array<User> $shareUsers\n * @param array<Group> $shareGroups\n */\n public function createPrompt(\n User $user,\n AskAnythingPromptTarget $target,\n string $title,\n string $content,\n array $shareUsers,\n array $shareGroups,\n ): AskAnythingPrompt {\n $prompt = AskAnythingPrompt::create([\n 'title' => $title,\n 'content' => $content,\n 'target' => $target,\n 'owner_id' => $user->getId(),\n ]);\n\n UserAskAnythingPrompt::create([\n 'user_id' => $user->getId(),\n 'prompt_id' => $prompt->getId(),\n ]);\n\n foreach ($shareUsers as $shareUser) {\n UserAskAnythingPrompt::create([\n 'user_id' => $shareUser->getId(),\n 'prompt_id' => $prompt->getId(),\n ]);\n }\n\n foreach ($shareGroups as $shareGroup) {\n UserAskAnythingPrompt::create([\n 'group_id' => $shareGroup->getId(),\n 'prompt_id' => $prompt->getId(),\n ]);\n }\n\n return $prompt;\n }\n\n /**\n * @param array<User> $shareUsers\n * @param array<Group> $shareGroups\n */\n public function editPrompt(\n AskAnythingPrompt $prompt,\n string $title,\n string $content,\n array $shareUsers,\n array $shareGroups,\n ): AskAnythingPrompt {\n $prompt->update([\n 'title' => $title,\n 'content' => $content,\n ]);\n\n $previousUserPrompts = UserAskAnythingPrompt::query()\n ->where('prompt_id', $prompt->getId())\n ->whereNull('group_id')\n ->whereNotNull('user_id')\n ->whereNot('user_id', $prompt->getOwnerId())\n ->get();\n\n $previousGroupPrompts = UserAskAnythingPrompt::query()\n ->where('prompt_id', $prompt->getId())\n ->whereNotNull('group_id')\n ->whereNull('user_id')\n ->get();\n\n $shareUserPrompts = [];\n foreach ($shareUsers as $shareUser) {\n $shareUserPrompts[] = UserAskAnythingPrompt::create([\n 'user_id' => $shareUser->getId(),\n 'prompt_id' => $prompt->getId(),\n ]);\n }\n\n $shareGroupPrompts = [];\n foreach ($shareGroups as $shareGroup) {\n $shareGroupPrompts[] = UserAskAnythingPrompt::create([\n 'group_id' => $shareGroup->getId(),\n 'prompt_id' => $prompt->getId(),\n ]);\n }\n\n // Remove those users that are no longer added\n $diffUsers = $previousUserPrompts->diff($shareUserPrompts);\n foreach ($diffUsers as $previousUserPrompt) {\n $previousUserPrompt->delete();\n }\n\n // Remove those groups that are no longer added\n $diffGroups = $previousGroupPrompts->diff($shareGroupPrompts);\n foreach ($diffGroups as $previousGroupPrompt) {\n $previousGroupPrompt->delete();\n }\n\n return $prompt;\n }\n\n public function deletePrompt(AskAnythingPrompt $prompt): void\n {\n // Also deletes all associations with users\n $prompt->delete();\n }\n\n public function hidePromptForUser(AskAnythingPrompt $prompt, User $user): AskAnythingPrompt\n {\n $userPromptSettings = UserAskAnythingPrompt::where('user_id', $user->getId())\n ->where('prompt_id', $prompt->getId())\n ->first();\n\n if ($userPromptSettings === null) {\n $userPromptSettings = UserAskAnythingPrompt::create([\n 'user_id' => $user->getId(),\n 'prompt_id' => $prompt->getId(),\n ]);\n }\n\n $userPromptSettings->update([\n 'is_removed' => true,\n ]);\n\n return $prompt;\n }\n\n public function getPromptByUuid(string $uuid): ?AskAnythingPrompt\n {\n return AskAnythingPrompt::where('uuid', AskAnythingPrompt::toOptimized($uuid))->first();\n }\n\n public function orderPromptForUser(AskAnythingPrompt $prompt, User $user, int $order): void\n {\n $userPromptSettings = UserAskAnythingPrompt::where('user_id', $user->getId())\n ->where('prompt_id', $prompt->getId())\n ->first();\n\n if ($userPromptSettings === null) {\n $userPromptSettings = UserAskAnythingPrompt::create([\n 'user_id' => $user->getId(),\n 'prompt_id' => $prompt->getId(),\n ]);\n }\n\n $userPromptSettings->update([\n 'order' => $order,\n ]);\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.42785904,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.43650267,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.4474734,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.45611703,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.46476063,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.47573137,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.4867021,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"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.51329786,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.5242686,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
5372536019564956971
|
6559418511826338412
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskAnythingPromptServiceTest
Run 'AskAnythingPromptServiceTest'
Debug 'AskAnythingPromptServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AskAnything\UserAskAnythingPrompt;
use Jiminny\Models\Group;
use Jiminny\Models\User;
class AskAnythingRepository
{
/**
* @return Collection<UserAskAnythingPrompt>
*/
public function findSharedUsersAndGroupsByPromptId(int $promptId): Collection
{
return UserAskAnythingPrompt::query()
->where('prompt_id', $promptId)
->where('is_removed', false)
->get();
}
public function findSharedPromptByUser(int $promptId, User $user): ?UserAskAnythingPrompt
{
return UserAskAnythingPrompt::with('prompt')
->where('prompt_id', $promptId)
->where('user_id', $user->getId())
->first();
}
public function findSharedPromptByUserGroup(int $promptId, User $user): ?UserAskAnythingPrompt
{
$userGroupId = $user->getGroupId();
return UserAskAnythingPrompt::with('prompt')
->where('prompt_id', $promptId)
->where(static function ($query) use ($userGroupId): void {
if ($userGroupId !== null) {
$query->where('group_id', $userGroupId);
}
})
->first();
}
/**
* @return Collection<AskAnythingPrompt>
*/
public function findPromptsByUserAndTarget(User $user, AskAnythingPromptTarget $target): Collection
{
$userGroupId = $user->getGroupId();
$usersOwnedPrompts = UserAskAnythingPrompt::with('prompt')
->where(static function ($query) use ($user, $userGroupId): void {
$query
->where('user_id', $user->getId());
if ($userGroupId !== null) {
$query->orWhere('group_id', $userGroupId);
}
})
->where('is_removed', false)
->whereHas('prompt', function (Builder $query) use ($target) {
$query->where('target', $target);
})
->orderByRaw('ISNULL(`order`), `order` ASC, `prompt_id` ASC')
->get()
->map(function (UserAskAnythingPrompt $userPrompt) {
return $userPrompt->getPrompt();
});
// Remove those prompts that are hidden for the current user
$usersOwnedPromptsFiltered = $usersOwnedPrompts->filter(function (AskAnythingPrompt $userPrompt) use ($user) {
$promptId = $userPrompt->getId();
$userDisabledPrompt = UserAskAnythingPrompt::query()
->where('prompt_id', $promptId)
->where('is_removed', true)
->where('user_id', $user->getId())
->first();
return $userDisabledPrompt === null;
});
$defaultNonChangedPrompts = AskAnythingPrompt::where('target', $target)
->whereDoesntHave('userPrompts', function ($query) use ($user) {
$query->where('user_id', $user->getId());
})
->whereNull('owner_id')
->get();
$allPrompts = $defaultNonChangedPrompts->merge($usersOwnedPromptsFiltered);
if ($allPrompts->isNotEmpty()) {
$allPrompts->loadCount('automatedReports');
}
return $allPrompts;
}
/**
* @param array<User> $shareUsers
* @param array<Group> $shareGroups
*/
public function createPrompt(
User $user,
AskAnythingPromptTarget $target,
string $title,
string $content,
array $shareUsers,
array $shareGroups,
): AskAnythingPrompt {
$prompt = AskAnythingPrompt::create([
'title' => $title,
'content' => $content,
'target' => $target,
'owner_id' => $user->getId(),
]);
UserAskAnythingPrompt::create([
'user_id' => $user->getId(),
'prompt_id' => $prompt->getId(),
]);
foreach ($shareUsers as $shareUser) {
UserAskAnythingPrompt::create([
'user_id' => $shareUser->getId(),
'prompt_id' => $prompt->getId(),
]);
}
foreach ($shareGroups as $shareGroup) {
UserAskAnythingPrompt::create([
'group_id' => $shareGroup->getId(),
'prompt_id' => $prompt->getId(),
]);
}
return $prompt;
}
/**
* @param array<User> $shareUsers
* @param array<Group> $shareGroups
*/
public function editPrompt(
AskAnythingPrompt $prompt,
string $title,
string $content,
array $shareUsers,
array $shareGroups,
): AskAnythingPrompt {
$prompt->update([
'title' => $title,
'content' => $content,
]);
$previousUserPrompts = UserAskAnythingPrompt::query()
->where('prompt_id', $prompt->getId())
->whereNull('group_id')
->whereNotNull('user_id')
->whereNot('user_id', $prompt->getOwnerId())
->get();
$previousGroupPrompts = UserAskAnythingPrompt::query()
->where('prompt_id', $prompt->getId())
->whereNotNull('group_id')
->whereNull('user_id')
->get();
$shareUserPrompts = [];
foreach ($shareUsers as $shareUser) {
$shareUserPrompts[] = UserAskAnythingPrompt::create([
'user_id' => $shareUser->getId(),
'prompt_id' => $prompt->getId(),
]);
}
$shareGroupPrompts = [];
foreach ($shareGroups as $shareGroup) {
$shareGroupPrompts[] = UserAskAnythingPrompt::create([
'group_id' => $shareGroup->getId(),
'prompt_id' => $prompt->getId(),
]);
}
// Remove those users that are no longer added
$diffUsers = $previousUserPrompts->diff($shareUserPrompts);
foreach ($diffUsers as $previousUserPrompt) {
$previousUserPrompt->delete();
}
// Remove those groups that are no longer added
$diffGroups = $previousGroupPrompts->diff($shareGroupPrompts);
foreach ($diffGroups as $previousGroupPrompt) {
$previousGroupPrompt->delete();
}
return $prompt;
}
public function deletePrompt(AskAnythingPrompt $prompt): void
{
// Also deletes all associations with users
$prompt->delete();
}
public function hidePromptForUser(AskAnythingPrompt $prompt, User $user): AskAnythingPrompt
{
$userPromptSettings = UserAskAnythingPrompt::where('user_id', $user->getId())
->where('prompt_id', $prompt->getId())
->first();
if ($userPromptSettings === null) {
$userPromptSettings = UserAskAnythingPrompt::create([
'user_id' => $user->getId(),
'prompt_id' => $prompt->getId(),
]);
}
$userPromptSettings->update([
'is_removed' => true,
]);
return $prompt;
}
public function getPromptByUuid(string $uuid): ?AskAnythingPrompt
{
return AskAnythingPrompt::where('uuid', AskAnythingPrompt::toOptimized($uuid))->first();
}
public function orderPromptForUser(AskAnythingPrompt $prompt, User $user, int $order): void
{
$userPromptSettings = UserAskAnythingPrompt::where('user_id', $user->getId())
->where('prompt_id', $prompt->getId())
->first();
if ($userPromptSettings === null) {
$userPromptSettings = UserAskAnythingPrompt::create([
'user_id' => $user->getId(),
'prompt_id' => $prompt->getId(),
]);
}
$userPromptSettings->update([
'order' => $order,
]);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground...
|
58051
|
NULL
|
NULL
|
NULL
|
|
58053
|
2042
|
19
|
2026-05-19T11:30:29.443054+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190229443_m1.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingRepository.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskAnythingPromptServiceTest
Run 'AskAnythingPromptServiceTest'
Debug 'AskAnythingPromptServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AskAnything\UserAskAnythingPrompt;
use Jiminny\Models\Group;
use Jiminny\Models\User;
class AskAnythingRepository
{
/**
* @return Collection<UserAskAnythingPrompt>
*/
public function findSharedUsersAndGroupsByPromptId(int $promptId): Collection
{
return UserAskAnythingPrompt::query()
->where('prompt_id', $promptId)
->where('is_removed', false)
->get();
}
public function findSharedPromptByUser(int $promptId, User $user): ?UserAskAnythingPrompt
{
return UserAskAnythingPrompt::with('prompt')
->where('prompt_id', $promptId)
->where('user_id', $user->getId())
->first();
}
public function findSharedPromptByUserGroup(int $promptId, User $user): ?UserAskAnythingPrompt
{
$userGroupId = $user->getGroupId();
return UserAskAnythingPrompt::with('prompt')
->where('prompt_id', $promptId)
->where(static function ($query) use ($userGroupId): void {
if ($userGroupId !== null) {
$query->where('group_id', $userGroupId);
}
})
->first();
}
/**
* @return Collection<AskAnythingPrompt>
*/
public function findPromptsByUserAndTarget(User $user, AskAnythingPromptTarget $target): Collection
{
$userGroupId = $user->getGroupId();
$usersOwnedPrompts = UserAskAnythingPrompt::with('prompt')
->where(static function ($query) use ($user, $userGroupId): void {
$query
->where('user_id', $user->getId());
if ($userGroupId !== null) {
$query->orWhere('group_id', $userGroupId);
}
})
->where('is_removed', false)
->whereHas('prompt', function (Builder $query) use ($target) {
$query->where('target', $target);
})
->orderByRaw('ISNULL(`order`), `order` ASC, `prompt_id` ASC')
->get()
->map(function (UserAskAnythingPrompt $userPrompt) {
return $userPrompt->getPrompt();
});
// Remove those prompts that are hidden for the current user
$usersOwnedPromptsFiltered = $usersOwnedPrompts->filter(function (AskAnythingPrompt $userPrompt) use ($user) {
$promptId = $userPrompt->getId();
$userDisabledPrompt = UserAskAnythingPrompt::query()
->where('prompt_id', $promptId)
->where('is_removed', true)
->where('user_id', $user->getId())
->first();
return $userDisabledPrompt === null;
});
$defaultNonChangedPrompts = AskAnythingPrompt::where('target', $target)
->whereDoesntHave('userPrompts', function ($query) use ($user) {
$query->where('user_id', $user->getId());
})
->whereNull('owner_id')
->get();
$allPrompts = $defaultNonChangedPrompts->merge($usersOwnedPromptsFiltered);
if ($allPrompts->isNotEmpty()) {
$allPrompts->loadCount('automatedReports');
}
return $allPrompts;
}
/**
* @param array<User> $shareUsers
* @param array<Group> $shareGroups
*/
public function createPrompt(
User $user,
AskAnythingPromptTarget $target,
string $title,
string $content,
array $shareUsers,
array $shareGroups,
): AskAnythingPrompt {
$prompt = AskAnythingPrompt::create([
'title' => $title,
'content' => $content,
'target' => $target,
'owner_id' => $user->getId(),
]);
UserAskAnythingPrompt::create([
'user_id' => $user->getId(),
'prompt_id' => $prompt->getId(),
]);
foreach ($shareUsers as $shareUser) {
UserAskAnythingPrompt::create([
'user_id' => $shareUser->getId(),
'prompt_id' => $prompt->getId(),
]);
}
foreach ($shareGroups as $shareGroup) {
UserAskAnythingPrompt::create([
'group_id' => $shareGroup->getId(),
'prompt_id' => $prompt->getId(),
]);
}
return $prompt;
}
/**
* @param array<User> $shareUsers
* @param array<Group> $shareGroups
*/
public function editPrompt(
AskAnythingPrompt $prompt,
string $title,
string $content,
array $shareUsers,
array $shareGroups,
): AskAnythingPrompt {
$prompt->update([
'title' => $title,
'content' => $content,
]);
$previousUserPrompts = UserAskAnythingPrompt::query()
->where('prompt_id', $prompt->getId())
->whereNull('group_id')
->whereNotNull('user_id')
->whereNot('user_id', $prompt->getOwnerId())
->get();
$previousGroupPrompts = UserAskAnythingPrompt::query()
->where('prompt_id', $prompt->getId())
->whereNotNull('group_id')
->whereNull('user_id')
->get();
$shareUserPrompts = [];
foreach ($shareUsers as $shareUser) {
$shareUserPrompts[] = UserAskAnythingPrompt::create([
'user_id' => $shareUser->getId(),
'prompt_id' => $prompt->getId(),
]);
}
$shareGroupPrompts = [];
foreach ($shareGroups as $shareGroup) {
$shareGroupPrompts[] = UserAskAnythingPrompt::create([
'group_id' => $shareGroup->getId(),
'prompt_id' => $prompt->getId(),
]);
}
// Remove those users that are no longer added
$diffUsers = $previousUserPrompts->diff($shareUserPrompts);
foreach ($diffUsers as $previousUserPrompt) {
$previousUserPrompt->delete();
}
// Remove those groups that are no longer added
$diffGroups = $previousGroupPrompts->diff($shareGroupPrompts);
foreach ($diffGroups as $previousGroupPrompt) {
$previousGroupPrompt->delete();
}
return $prompt;
}
public function deletePrompt(AskAnythingPrompt $prompt): void
{
// Also deletes all associations with users
$prompt->delete();
}
public function hidePromptForUser(AskAnythingPrompt $prompt, User $user): AskAnythingPrompt
{
$userPromptSettings = UserAskAnythingPrompt::where('user_id', $user->getId())
->where('prompt_id', $prompt->getId())
->first();
if ($userPromptSettings === null) {
$userPromptSettings = UserAskAnythingPrompt::create([
'user_id' => $user->getId(),
'prompt_id' => $prompt->getId(),
]);
}
$userPromptSettings->update([
'is_removed' => true,
]);
return $prompt;
}
public function getPromptByUuid(string $uuid): ?AskAnythingPrompt
{
return AskAnythingPrompt::where('uuid', AskAnythingPrompt::toOptimized($uuid))->first();
}
public function orderPromptForUser(AskAnythingPrompt $prompt, User $user, int $order): void
{
$userPromptSettings = UserAskAnythingPrompt::where('user_id', $user->getId())
->where('prompt_id', $prompt->getId())
->first();
if ($userPromptSettings === null) {
$userPromptSettings = UserAskAnythingPrompt::create([
'user_id' => $user->getId(),
'prompt_id' => $prompt->getId(),
]);
}
$userPromptSettings->update([
'order' => $order,
]);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Code changed:
Hide
Sync Changes
Hide This Notification
21
1
18
2
6
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 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 * 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 `automated_report_results`.* FROM `automated_report_results`
INNER JOIN `automated_reports`
ON `automated_report_results`.`report_id` = `automated_reports`.`id`
WHERE 1=1
AND `automated_report_results`.`generated_at` IS NOT NULL
# AND `automated_report_results`.`sent_at` IS NOT NULL
AND `automated_reports`.`team_id` = 1
AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$."users"')
;
SELECT * FROM automated_reports where id = 67;
SELECT * FROM automated_reports where id = 42;
SELECT * FROM users WHERE id = 143; # group 28
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;
SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003
SELECT * FROM activities WHERE id IN (16,422003);
SELECT * FROM activities where status = 'failed';
SELECT * FROM tracks WHERE activity_id = 422003;
SELECT
a.*
FROM activities a
JOIN users u ON a.user_id = u.id
WHERE
a.status = 'completed'
AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid
AND a.deleted_at IS NULL
AND EXISTS (
SELECT 1 FROM tracks t
WHERE t.activity_id = a.id
AND t.type IN ('audio', 'video')
)
ORDER BY a.actual_start_time DESC
LIMIT 25;
select * from teams where id = 19;
select * from crm_configurations where provider = 'pipedrive';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 19 and sa.provider = 'pipedrive';
SELECT * FROM social_accounts WHERE id = 1116;
UPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',
provider_refresh_token = '5034113:[TELEGRAM_TOKEN]b2bfc',
expires = 1779091997,
state = 'connected'
WHERE id = 1116;
"provider_user_token": "v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA",
"provider_refresh_token": "5034113:[TELEGRAM_TOKEN]b2bfc",
"expires": 1779091997,
Socket fail to connect to host:address=(host=localhost)(port=3306)(type=primary). Connection refused
Project
Project
New File or Directory…
Expand Selected
Collapse All...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects<br/>Some incoming commits are not fetched<br/>","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,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskAnythingPromptServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskAnythingPromptServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskAnythingPromptServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"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 Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AskAnything\\UserAskAnythingPrompt;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\User;\n\nclass AskAnythingRepository\n{\n /**\n * @return Collection<UserAskAnythingPrompt>\n */\n public function findSharedUsersAndGroupsByPromptId(int $promptId): Collection\n {\n return UserAskAnythingPrompt::query()\n ->where('prompt_id', $promptId)\n ->where('is_removed', false)\n ->get();\n }\n\n public function findSharedPromptByUser(int $promptId, User $user): ?UserAskAnythingPrompt\n {\n return UserAskAnythingPrompt::with('prompt')\n ->where('prompt_id', $promptId)\n ->where('user_id', $user->getId())\n ->first();\n }\n\n public function findSharedPromptByUserGroup(int $promptId, User $user): ?UserAskAnythingPrompt\n {\n $userGroupId = $user->getGroupId();\n\n return UserAskAnythingPrompt::with('prompt')\n ->where('prompt_id', $promptId)\n ->where(static function ($query) use ($userGroupId): void {\n if ($userGroupId !== null) {\n $query->where('group_id', $userGroupId);\n }\n })\n ->first();\n }\n\n /**\n * @return Collection<AskAnythingPrompt>\n */\n public function findPromptsByUserAndTarget(User $user, AskAnythingPromptTarget $target): Collection\n {\n $userGroupId = $user->getGroupId();\n $usersOwnedPrompts = UserAskAnythingPrompt::with('prompt')\n ->where(static function ($query) use ($user, $userGroupId): void {\n $query\n ->where('user_id', $user->getId());\n\n if ($userGroupId !== null) {\n $query->orWhere('group_id', $userGroupId);\n }\n })\n ->where('is_removed', false)\n ->whereHas('prompt', function (Builder $query) use ($target) {\n $query->where('target', $target);\n })\n ->orderByRaw('ISNULL(`order`), `order` ASC, `prompt_id` ASC')\n ->get()\n ->map(function (UserAskAnythingPrompt $userPrompt) {\n return $userPrompt->getPrompt();\n });\n\n // Remove those prompts that are hidden for the current user\n $usersOwnedPromptsFiltered = $usersOwnedPrompts->filter(function (AskAnythingPrompt $userPrompt) use ($user) {\n $promptId = $userPrompt->getId();\n $userDisabledPrompt = UserAskAnythingPrompt::query()\n ->where('prompt_id', $promptId)\n ->where('is_removed', true)\n ->where('user_id', $user->getId())\n ->first();\n\n return $userDisabledPrompt === null;\n });\n\n $defaultNonChangedPrompts = AskAnythingPrompt::where('target', $target)\n ->whereDoesntHave('userPrompts', function ($query) use ($user) {\n $query->where('user_id', $user->getId());\n })\n ->whereNull('owner_id')\n ->get();\n\n $allPrompts = $defaultNonChangedPrompts->merge($usersOwnedPromptsFiltered);\n\n if ($allPrompts->isNotEmpty()) {\n $allPrompts->loadCount('automatedReports');\n }\n\n return $allPrompts;\n }\n\n /**\n * @param array<User> $shareUsers\n * @param array<Group> $shareGroups\n */\n public function createPrompt(\n User $user,\n AskAnythingPromptTarget $target,\n string $title,\n string $content,\n array $shareUsers,\n array $shareGroups,\n ): AskAnythingPrompt {\n $prompt = AskAnythingPrompt::create([\n 'title' => $title,\n 'content' => $content,\n 'target' => $target,\n 'owner_id' => $user->getId(),\n ]);\n\n UserAskAnythingPrompt::create([\n 'user_id' => $user->getId(),\n 'prompt_id' => $prompt->getId(),\n ]);\n\n foreach ($shareUsers as $shareUser) {\n UserAskAnythingPrompt::create([\n 'user_id' => $shareUser->getId(),\n 'prompt_id' => $prompt->getId(),\n ]);\n }\n\n foreach ($shareGroups as $shareGroup) {\n UserAskAnythingPrompt::create([\n 'group_id' => $shareGroup->getId(),\n 'prompt_id' => $prompt->getId(),\n ]);\n }\n\n return $prompt;\n }\n\n /**\n * @param array<User> $shareUsers\n * @param array<Group> $shareGroups\n */\n public function editPrompt(\n AskAnythingPrompt $prompt,\n string $title,\n string $content,\n array $shareUsers,\n array $shareGroups,\n ): AskAnythingPrompt {\n $prompt->update([\n 'title' => $title,\n 'content' => $content,\n ]);\n\n $previousUserPrompts = UserAskAnythingPrompt::query()\n ->where('prompt_id', $prompt->getId())\n ->whereNull('group_id')\n ->whereNotNull('user_id')\n ->whereNot('user_id', $prompt->getOwnerId())\n ->get();\n\n $previousGroupPrompts = UserAskAnythingPrompt::query()\n ->where('prompt_id', $prompt->getId())\n ->whereNotNull('group_id')\n ->whereNull('user_id')\n ->get();\n\n $shareUserPrompts = [];\n foreach ($shareUsers as $shareUser) {\n $shareUserPrompts[] = UserAskAnythingPrompt::create([\n 'user_id' => $shareUser->getId(),\n 'prompt_id' => $prompt->getId(),\n ]);\n }\n\n $shareGroupPrompts = [];\n foreach ($shareGroups as $shareGroup) {\n $shareGroupPrompts[] = UserAskAnythingPrompt::create([\n 'group_id' => $shareGroup->getId(),\n 'prompt_id' => $prompt->getId(),\n ]);\n }\n\n // Remove those users that are no longer added\n $diffUsers = $previousUserPrompts->diff($shareUserPrompts);\n foreach ($diffUsers as $previousUserPrompt) {\n $previousUserPrompt->delete();\n }\n\n // Remove those groups that are no longer added\n $diffGroups = $previousGroupPrompts->diff($shareGroupPrompts);\n foreach ($diffGroups as $previousGroupPrompt) {\n $previousGroupPrompt->delete();\n }\n\n return $prompt;\n }\n\n public function deletePrompt(AskAnythingPrompt $prompt): void\n {\n // Also deletes all associations with users\n $prompt->delete();\n }\n\n public function hidePromptForUser(AskAnythingPrompt $prompt, User $user): AskAnythingPrompt\n {\n $userPromptSettings = UserAskAnythingPrompt::where('user_id', $user->getId())\n ->where('prompt_id', $prompt->getId())\n ->first();\n\n if ($userPromptSettings === null) {\n $userPromptSettings = UserAskAnythingPrompt::create([\n 'user_id' => $user->getId(),\n 'prompt_id' => $prompt->getId(),\n ]);\n }\n\n $userPromptSettings->update([\n 'is_removed' => true,\n ]);\n\n return $prompt;\n }\n\n public function getPromptByUuid(string $uuid): ?AskAnythingPrompt\n {\n return AskAnythingPrompt::where('uuid', AskAnythingPrompt::toOptimized($uuid))->first();\n }\n\n public function orderPromptForUser(AskAnythingPrompt $prompt, User $user, int $order): void\n {\n $userPromptSettings = UserAskAnythingPrompt::where('user_id', $user->getId())\n ->where('prompt_id', $prompt->getId())\n ->first();\n\n if ($userPromptSettings === null) {\n $userPromptSettings = UserAskAnythingPrompt::create([\n 'user_id' => $user->getId(),\n 'prompt_id' => $prompt->getId(),\n ]);\n }\n\n $userPromptSettings->update([\n 'order' => $order,\n ]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AskAnything\\UserAskAnythingPrompt;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\User;\n\nclass AskAnythingRepository\n{\n /**\n * @return Collection<UserAskAnythingPrompt>\n */\n public function findSharedUsersAndGroupsByPromptId(int $promptId): Collection\n {\n return UserAskAnythingPrompt::query()\n ->where('prompt_id', $promptId)\n ->where('is_removed', false)\n ->get();\n }\n\n public function findSharedPromptByUser(int $promptId, User $user): ?UserAskAnythingPrompt\n {\n return UserAskAnythingPrompt::with('prompt')\n ->where('prompt_id', $promptId)\n ->where('user_id', $user->getId())\n ->first();\n }\n\n public function findSharedPromptByUserGroup(int $promptId, User $user): ?UserAskAnythingPrompt\n {\n $userGroupId = $user->getGroupId();\n\n return UserAskAnythingPrompt::with('prompt')\n ->where('prompt_id', $promptId)\n ->where(static function ($query) use ($userGroupId): void {\n if ($userGroupId !== null) {\n $query->where('group_id', $userGroupId);\n }\n })\n ->first();\n }\n\n /**\n * @return Collection<AskAnythingPrompt>\n */\n public function findPromptsByUserAndTarget(User $user, AskAnythingPromptTarget $target): Collection\n {\n $userGroupId = $user->getGroupId();\n $usersOwnedPrompts = UserAskAnythingPrompt::with('prompt')\n ->where(static function ($query) use ($user, $userGroupId): void {\n $query\n ->where('user_id', $user->getId());\n\n if ($userGroupId !== null) {\n $query->orWhere('group_id', $userGroupId);\n }\n })\n ->where('is_removed', false)\n ->whereHas('prompt', function (Builder $query) use ($target) {\n $query->where('target', $target);\n })\n ->orderByRaw('ISNULL(`order`), `order` ASC, `prompt_id` ASC')\n ->get()\n ->map(function (UserAskAnythingPrompt $userPrompt) {\n return $userPrompt->getPrompt();\n });\n\n // Remove those prompts that are hidden for the current user\n $usersOwnedPromptsFiltered = $usersOwnedPrompts->filter(function (AskAnythingPrompt $userPrompt) use ($user) {\n $promptId = $userPrompt->getId();\n $userDisabledPrompt = UserAskAnythingPrompt::query()\n ->where('prompt_id', $promptId)\n ->where('is_removed', true)\n ->where('user_id', $user->getId())\n ->first();\n\n return $userDisabledPrompt === null;\n });\n\n $defaultNonChangedPrompts = AskAnythingPrompt::where('target', $target)\n ->whereDoesntHave('userPrompts', function ($query) use ($user) {\n $query->where('user_id', $user->getId());\n })\n ->whereNull('owner_id')\n ->get();\n\n $allPrompts = $defaultNonChangedPrompts->merge($usersOwnedPromptsFiltered);\n\n if ($allPrompts->isNotEmpty()) {\n $allPrompts->loadCount('automatedReports');\n }\n\n return $allPrompts;\n }\n\n /**\n * @param array<User> $shareUsers\n * @param array<Group> $shareGroups\n */\n public function createPrompt(\n User $user,\n AskAnythingPromptTarget $target,\n string $title,\n string $content,\n array $shareUsers,\n array $shareGroups,\n ): AskAnythingPrompt {\n $prompt = AskAnythingPrompt::create([\n 'title' => $title,\n 'content' => $content,\n 'target' => $target,\n 'owner_id' => $user->getId(),\n ]);\n\n UserAskAnythingPrompt::create([\n 'user_id' => $user->getId(),\n 'prompt_id' => $prompt->getId(),\n ]);\n\n foreach ($shareUsers as $shareUser) {\n UserAskAnythingPrompt::create([\n 'user_id' => $shareUser->getId(),\n 'prompt_id' => $prompt->getId(),\n ]);\n }\n\n foreach ($shareGroups as $shareGroup) {\n UserAskAnythingPrompt::create([\n 'group_id' => $shareGroup->getId(),\n 'prompt_id' => $prompt->getId(),\n ]);\n }\n\n return $prompt;\n }\n\n /**\n * @param array<User> $shareUsers\n * @param array<Group> $shareGroups\n */\n public function editPrompt(\n AskAnythingPrompt $prompt,\n string $title,\n string $content,\n array $shareUsers,\n array $shareGroups,\n ): AskAnythingPrompt {\n $prompt->update([\n 'title' => $title,\n 'content' => $content,\n ]);\n\n $previousUserPrompts = UserAskAnythingPrompt::query()\n ->where('prompt_id', $prompt->getId())\n ->whereNull('group_id')\n ->whereNotNull('user_id')\n ->whereNot('user_id', $prompt->getOwnerId())\n ->get();\n\n $previousGroupPrompts = UserAskAnythingPrompt::query()\n ->where('prompt_id', $prompt->getId())\n ->whereNotNull('group_id')\n ->whereNull('user_id')\n ->get();\n\n $shareUserPrompts = [];\n foreach ($shareUsers as $shareUser) {\n $shareUserPrompts[] = UserAskAnythingPrompt::create([\n 'user_id' => $shareUser->getId(),\n 'prompt_id' => $prompt->getId(),\n ]);\n }\n\n $shareGroupPrompts = [];\n foreach ($shareGroups as $shareGroup) {\n $shareGroupPrompts[] = UserAskAnythingPrompt::create([\n 'group_id' => $shareGroup->getId(),\n 'prompt_id' => $prompt->getId(),\n ]);\n }\n\n // Remove those users that are no longer added\n $diffUsers = $previousUserPrompts->diff($shareUserPrompts);\n foreach ($diffUsers as $previousUserPrompt) {\n $previousUserPrompt->delete();\n }\n\n // Remove those groups that are no longer added\n $diffGroups = $previousGroupPrompts->diff($shareGroupPrompts);\n foreach ($diffGroups as $previousGroupPrompt) {\n $previousGroupPrompt->delete();\n }\n\n return $prompt;\n }\n\n public function deletePrompt(AskAnythingPrompt $prompt): void\n {\n // Also deletes all associations with users\n $prompt->delete();\n }\n\n public function hidePromptForUser(AskAnythingPrompt $prompt, User $user): AskAnythingPrompt\n {\n $userPromptSettings = UserAskAnythingPrompt::where('user_id', $user->getId())\n ->where('prompt_id', $prompt->getId())\n ->first();\n\n if ($userPromptSettings === null) {\n $userPromptSettings = UserAskAnythingPrompt::create([\n 'user_id' => $user->getId(),\n 'prompt_id' => $prompt->getId(),\n ]);\n }\n\n $userPromptSettings->update([\n 'is_removed' => true,\n ]);\n\n return $prompt;\n }\n\n public function getPromptByUuid(string $uuid): ?AskAnythingPrompt\n {\n return AskAnythingPrompt::where('uuid', AskAnythingPrompt::toOptimized($uuid))->first();\n }\n\n public function orderPromptForUser(AskAnythingPrompt $prompt, User $user, int $order): void\n {\n $userPromptSettings = UserAskAnythingPrompt::where('user_id', $user->getId())\n ->where('prompt_id', $prompt->getId())\n ->first();\n\n if ($userPromptSettings === null) {\n $userPromptSettings = UserAskAnythingPrompt::create([\n 'user_id' => $user->getId(),\n 'prompt_id' => $prompt->getId(),\n ]);\n }\n\n $userPromptSettings->update([\n 'order' => $order,\n ]);\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,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"21","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"18","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"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 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 * 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 `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\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;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","depth":4,"on_screen":true,"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 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 * 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 `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\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;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Socket fail to connect to host:address=(host=localhost)(port=3306)(type=primary). Connection refused","depth":3,"bounds":{"left":0.3263889,"top":0.0,"width":0.6125,"height":0.018888889},"on_screen":true,"value":"Socket fail to connect to host:address=(host=localhost)(port=3306)(type=primary). Connection refused","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"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},"on_screen":false,"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},"on_screen":false,"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},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-4743038955149528495
|
6830585209447388749
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskAnythingPromptServiceTest
Run 'AskAnythingPromptServiceTest'
Debug 'AskAnythingPromptServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AskAnything\UserAskAnythingPrompt;
use Jiminny\Models\Group;
use Jiminny\Models\User;
class AskAnythingRepository
{
/**
* @return Collection<UserAskAnythingPrompt>
*/
public function findSharedUsersAndGroupsByPromptId(int $promptId): Collection
{
return UserAskAnythingPrompt::query()
->where('prompt_id', $promptId)
->where('is_removed', false)
->get();
}
public function findSharedPromptByUser(int $promptId, User $user): ?UserAskAnythingPrompt
{
return UserAskAnythingPrompt::with('prompt')
->where('prompt_id', $promptId)
->where('user_id', $user->getId())
->first();
}
public function findSharedPromptByUserGroup(int $promptId, User $user): ?UserAskAnythingPrompt
{
$userGroupId = $user->getGroupId();
return UserAskAnythingPrompt::with('prompt')
->where('prompt_id', $promptId)
->where(static function ($query) use ($userGroupId): void {
if ($userGroupId !== null) {
$query->where('group_id', $userGroupId);
}
})
->first();
}
/**
* @return Collection<AskAnythingPrompt>
*/
public function findPromptsByUserAndTarget(User $user, AskAnythingPromptTarget $target): Collection
{
$userGroupId = $user->getGroupId();
$usersOwnedPrompts = UserAskAnythingPrompt::with('prompt')
->where(static function ($query) use ($user, $userGroupId): void {
$query
->where('user_id', $user->getId());
if ($userGroupId !== null) {
$query->orWhere('group_id', $userGroupId);
}
})
->where('is_removed', false)
->whereHas('prompt', function (Builder $query) use ($target) {
$query->where('target', $target);
})
->orderByRaw('ISNULL(`order`), `order` ASC, `prompt_id` ASC')
->get()
->map(function (UserAskAnythingPrompt $userPrompt) {
return $userPrompt->getPrompt();
});
// Remove those prompts that are hidden for the current user
$usersOwnedPromptsFiltered = $usersOwnedPrompts->filter(function (AskAnythingPrompt $userPrompt) use ($user) {
$promptId = $userPrompt->getId();
$userDisabledPrompt = UserAskAnythingPrompt::query()
->where('prompt_id', $promptId)
->where('is_removed', true)
->where('user_id', $user->getId())
->first();
return $userDisabledPrompt === null;
});
$defaultNonChangedPrompts = AskAnythingPrompt::where('target', $target)
->whereDoesntHave('userPrompts', function ($query) use ($user) {
$query->where('user_id', $user->getId());
})
->whereNull('owner_id')
->get();
$allPrompts = $defaultNonChangedPrompts->merge($usersOwnedPromptsFiltered);
if ($allPrompts->isNotEmpty()) {
$allPrompts->loadCount('automatedReports');
}
return $allPrompts;
}
/**
* @param array<User> $shareUsers
* @param array<Group> $shareGroups
*/
public function createPrompt(
User $user,
AskAnythingPromptTarget $target,
string $title,
string $content,
array $shareUsers,
array $shareGroups,
): AskAnythingPrompt {
$prompt = AskAnythingPrompt::create([
'title' => $title,
'content' => $content,
'target' => $target,
'owner_id' => $user->getId(),
]);
UserAskAnythingPrompt::create([
'user_id' => $user->getId(),
'prompt_id' => $prompt->getId(),
]);
foreach ($shareUsers as $shareUser) {
UserAskAnythingPrompt::create([
'user_id' => $shareUser->getId(),
'prompt_id' => $prompt->getId(),
]);
}
foreach ($shareGroups as $shareGroup) {
UserAskAnythingPrompt::create([
'group_id' => $shareGroup->getId(),
'prompt_id' => $prompt->getId(),
]);
}
return $prompt;
}
/**
* @param array<User> $shareUsers
* @param array<Group> $shareGroups
*/
public function editPrompt(
AskAnythingPrompt $prompt,
string $title,
string $content,
array $shareUsers,
array $shareGroups,
): AskAnythingPrompt {
$prompt->update([
'title' => $title,
'content' => $content,
]);
$previousUserPrompts = UserAskAnythingPrompt::query()
->where('prompt_id', $prompt->getId())
->whereNull('group_id')
->whereNotNull('user_id')
->whereNot('user_id', $prompt->getOwnerId())
->get();
$previousGroupPrompts = UserAskAnythingPrompt::query()
->where('prompt_id', $prompt->getId())
->whereNotNull('group_id')
->whereNull('user_id')
->get();
$shareUserPrompts = [];
foreach ($shareUsers as $shareUser) {
$shareUserPrompts[] = UserAskAnythingPrompt::create([
'user_id' => $shareUser->getId(),
'prompt_id' => $prompt->getId(),
]);
}
$shareGroupPrompts = [];
foreach ($shareGroups as $shareGroup) {
$shareGroupPrompts[] = UserAskAnythingPrompt::create([
'group_id' => $shareGroup->getId(),
'prompt_id' => $prompt->getId(),
]);
}
// Remove those users that are no longer added
$diffUsers = $previousUserPrompts->diff($shareUserPrompts);
foreach ($diffUsers as $previousUserPrompt) {
$previousUserPrompt->delete();
}
// Remove those groups that are no longer added
$diffGroups = $previousGroupPrompts->diff($shareGroupPrompts);
foreach ($diffGroups as $previousGroupPrompt) {
$previousGroupPrompt->delete();
}
return $prompt;
}
public function deletePrompt(AskAnythingPrompt $prompt): void
{
// Also deletes all associations with users
$prompt->delete();
}
public function hidePromptForUser(AskAnythingPrompt $prompt, User $user): AskAnythingPrompt
{
$userPromptSettings = UserAskAnythingPrompt::where('user_id', $user->getId())
->where('prompt_id', $prompt->getId())
->first();
if ($userPromptSettings === null) {
$userPromptSettings = UserAskAnythingPrompt::create([
'user_id' => $user->getId(),
'prompt_id' => $prompt->getId(),
]);
}
$userPromptSettings->update([
'is_removed' => true,
]);
return $prompt;
}
public function getPromptByUuid(string $uuid): ?AskAnythingPrompt
{
return AskAnythingPrompt::where('uuid', AskAnythingPrompt::toOptimized($uuid))->first();
}
public function orderPromptForUser(AskAnythingPrompt $prompt, User $user, int $order): void
{
$userPromptSettings = UserAskAnythingPrompt::where('user_id', $user->getId())
->where('prompt_id', $prompt->getId())
->first();
if ($userPromptSettings === null) {
$userPromptSettings = UserAskAnythingPrompt::create([
'user_id' => $user->getId(),
'prompt_id' => $prompt->getId(),
]);
}
$userPromptSettings->update([
'order' => $order,
]);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Code changed:
Hide
Sync Changes
Hide This Notification
21
1
18
2
6
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 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 * 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 `automated_report_results`.* FROM `automated_report_results`
INNER JOIN `automated_reports`
ON `automated_report_results`.`report_id` = `automated_reports`.`id`
WHERE 1=1
AND `automated_report_results`.`generated_at` IS NOT NULL
# AND `automated_report_results`.`sent_at` IS NOT NULL
AND `automated_reports`.`team_id` = 1
AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$."users"')
;
SELECT * FROM automated_reports where id = 67;
SELECT * FROM automated_reports where id = 42;
SELECT * FROM users WHERE id = 143; # group 28
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;
SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003
SELECT * FROM activities WHERE id IN (16,422003);
SELECT * FROM activities where status = 'failed';
SELECT * FROM tracks WHERE activity_id = 422003;
SELECT
a.*
FROM activities a
JOIN users u ON a.user_id = u.id
WHERE
a.status = 'completed'
AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid
AND a.deleted_at IS NULL
AND EXISTS (
SELECT 1 FROM tracks t
WHERE t.activity_id = a.id
AND t.type IN ('audio', 'video')
)
ORDER BY a.actual_start_time DESC
LIMIT 25;
select * from teams where id = 19;
select * from crm_configurations where provider = 'pipedrive';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 19 and sa.provider = 'pipedrive';
SELECT * FROM social_accounts WHERE id = 1116;
UPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',
provider_refresh_token = '5034113:[TELEGRAM_TOKEN]b2bfc',
expires = 1779091997,
state = 'connected'
WHERE id = 1116;
"provider_user_token": "v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA",
"provider_refresh_token": "5034113:[TELEGRAM_TOKEN]b2bfc",
"expires": 1779091997,
Socket fail to connect to host:address=(host=localhost)(port=3306)(type=primary). Connection refused
Project
Project
New File or Directory…
Expand Selected
Collapse All...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58056
|
2043
|
20
|
2026-05-19T11:30:32.736772+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190232736_m2.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingRepository.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormProiect vViewINavicareCodeLaravelRefactorT PhpStormProiect vViewINavicareCodeLaravelRefactorTOOISWindowFV faVsco.jsv?9 JY-20676-delete-report-related-objectsC ActivityController.ong© AskAnythingController.php=custom.log= laravel.log4 SF jiminny@localhost] x4 HS_local [jiminny@localhost]& console [PROD]# console [euy©EncryptionServiceProvider.php(©) FventServiceProvider.php© HubspotJournalServiceProvider.php© HubspotWebhookServiceProvider.php© JiminnyServiceProvider.php© PlanhatServiceProvider.php© ProphetHandlerServiceProvider.php© PusherServiceProvider.php© AskAnythingPromptService.php© AskAnythingRepository.php X© AutomatedReportsServiceTest.php© AskAnythingPromptDto.phpA console [STAGING]© CoachingFeedbackCoachUserln.phpTx: Autov© AskJiminnyReportsController.phg© AutomatedReportsService.php©) AskAny1gPromptservice lest.php© Search.phpSo jiminny v021 A1 A18 V2 V6 ^class ASkAnythingRepos1torvC) @ueueLoqServiceProvider.php© QueueStatsdServiceProvider.phpC) ResponseMacroServiceProvider.pho© RouteServiceProvider.phpc) SsoServiceProvider.pho@ UtilServiceProvider.ohnC) ViewerGuardServiceProvider.oho> D Queuev Revositories>MAiAutoScoringM Calendarv Merm© AccountRepository.php© ContactRepository.php(e) ContactRo leRepository.ong© CrmConfigurationRepository.php© CrmEntityRepository.php© FieldDataRepository.php© FieldRepository.php© LayoutEntityRepository.php© LayoutRepository.php© LeadRepository.php© OpportunityRepository.php© ProfileRepository.php© RecordTypeFieldValuesRepository.php@ StageRepositorv.php@ SvncBatchRepositorv.php>M GeographyC) ActiveStreamsReoositorv.oho© ActivityCommentRepository.phpC) ActivitvLoaReoositorv.oho©ActivityMessageRepository.phpC) ActivitvMomentRenositorv.nhnl©ActivityProviderRepository.phpC) [EMAIL]"C) ActivitvShareRenositorv [EMAIL](C) A:PromntRenositorv nhn(0 AckAnvthinaDonocitory nhn© AutomatedReportsRepository.php© CallImportRepositorv.phpshed 1 commit to oriain/IV.20676-delete-rend* dreturn collection<lseraskanuthinaPromot›A12 ^ v 182183SELECT ar.id, ar.uuid, ar.mediatype, ar.status, a.typeFROM automated_report_results arJOIN automated_reports a ON a.id = ar.reportidWHERE a.type ='ask_jiminny'LIMIT 10;public function findSharedUsersAndGroupsByPromptId(int $promptId): Collection(...}188 011 usagespublic function findSharedPromptByUser(int $promptid, User Suser): ?UserAskAnythingPromptt...}198SELECT * FROM automated reports where id = 71;SELECT * FROM automated report results where report id = 71:UPDATE automated reports set playbook categories = NULL where id = 68SELECT * FROM automated report results where id = 275;public function findSharedPromptByUserGroup(int $promptId, User Suser): ?UserAskAnythingPrompt{...]192193194* @return Collection<AskAnythingPrompt>SELECT * FROM autorated_reports order by za desc;SELECT * FROMautomated_reportresults order by 1d desc:select * from activity_searches here user_id = 143;Method Jiminny\Repositories\AskAnythingRepository.findPromptsByUserAndTarget AskAnythingRepository ../app/Reposinated report results.* FROM 'automated report resultsiEH→CiG • & Project Files vUsagested renort results', renort id = 'automated renorts'.'1di© AskAnythingPromptService.php© ReorderAskAnythingPromptsRequest.php 32© ReorderAskAnythingPromptsRequest.php 62C) AskAnythingPromptServiceTest.php$prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(->findPromptsByUserAndTarget(Sthis->user(), $this->getTarget())->findPromptsByUserAndTarget(Sthis->user(), Sthis->getTarget())ated_report_results. generated_at IS NOT NULLomated renort results' 'sent at' IS NOT NULU->metnod(TinaPromptsbyuserAnd larger)ated renorts' 'team 1d' = 1.ONTAINS( automated_reports. recipients, 143, '$."users"')->get@->man(function (UserAskAnvthingPrompt SuserPromot) {...})// Remove those prompts that are hidden for the current userSusens0wnedPromotsFiltered = Susens0wnedPromnts-sfilten(function (AskAnvthinaPromnt SusenPrompt 210SELECT * FROM automated_reports where id = 67:SELECT * FROM automated_reports where id = 42;SELECT * FROM users WHERE id = 143; # group 28$defaultNonChangedPrompts = AskAnythingPrompt::where('target', $target)-›whereDoesntHave('userPrompts', function ($query) use (Suser) {...})>whereNullownen id')->aetos213• 214215select * from teams where id = 3143;select * from crm_configurations where id = 500:select * from users where name = 'Integration Account': # 1695SELECT * FROM social accounts WHERE sociable id = 1695:$allPrompts = $defaultNonChangedPrompts->merge(Susers0wnedPromptsFiltered);select * from activities where crm_ configuration id = 39and recording state = 'recorded' and duration > 60and status = 'completed' and actual start time >= '2025-12-01':if ($allPrompts->isNotEmpty@) {$allPrompts->loadCount('automatedReports'):-220222-223SELECT * FROM activities WHERE uuid to bin('458cf915-b914-4000-b083-568763262956') = uuidiselect * from leadsireturn saluprompts._225* dparam arrau<user> Ssharelsers105* aparam arrau<Grouo> SsharebrouosiSELECT * FROM activities WHERE uuid to binc'f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid: # 422003SELECT * FROM activities WHERE id IN (16,422003):SSIECT * SROM activitios whenp status ='failed':Socket fail to connect to host:address=(host=localhost) (port=3306)(tvpe=primarv). Connection refusedectc /I View null reauect (todav 12:17)• suppont Dally • In 3um100% 5• Tue 19 May 14:30:32+0 ..CascadeAsk Jiminny Report MDo you neea sprompt->getHaskeportso in tne DIU?Yes tor the list oath — that's the whole reason for the tlao, current v in det O:1AskAnvthinaPromotService.oho:51-60$promptDtosl] = new AskAnythingPromptDto(Sprompt->gecuuzd),Spromot>aetTitleo.$prompt->getContent(),Sshareusers$prompt->getHasReports(),That's correct and already there. Thanks to the loadCount in the repository. this is now • (1) extra query for the whole list.What about edito?edit() currently doesn't set hasReports, which is fine and intentional:• The newly created prompt (in the isDefaultPrompt branch) is fresh - by definition zero reports.• The edited prompt may or may not have reports attached, but: editing the prompt doesn't add or remove reportassociations, so the trontend already knows the value from when the prompt was loaded in the list. Ine Pul responsedoesn't need to recomoute it.If you want the response to be consistent with create() (which | changed to explicitly send false), you have two options1. Leave edit() as-is — has reports is simply absent from the response. Cleanest semantically: "we didn't touch this:don't trust whatever vou tind here"2. Set hasReports: $newPrompt->getHasReports() — falls back to an exists() query (since loadCount wasn't run onthe sinale promnt). One eytra chean auery ner edit. Keens the API response shape uniform with the list endoointIf vou want ontion 2 here's the chanaeASKAnyrvice.php:152-161return new AskAnythingPromptDto(Snewprompt-geclitlel,SnewPromnt_>aetTaraet)).chlalredcatinc lmmaldsMy recommendation: leave edit alone. The frontend already has the value from the list, and skipping it avoids anunnecessary DB hit on every edit. The create() case got false only because a brand-new prompt is trivially known tohave no renorts — no auerv costwtoall «eAsk anvthing 884-D)<> Code Claude Qnus 4.7 MediumW Windsurf Teamc1-1.UTE.8Po 4 spaces...
|
NULL
|
7159502682844733372
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormProiect vViewINavicareCodeLaravelRefactorT PhpStormProiect vViewINavicareCodeLaravelRefactorTOOISWindowFV faVsco.jsv?9 JY-20676-delete-report-related-objectsC ActivityController.ong© AskAnythingController.php=custom.log= laravel.log4 SF jiminny@localhost] x4 HS_local [jiminny@localhost]& console [PROD]# console [euy©EncryptionServiceProvider.php(©) FventServiceProvider.php© HubspotJournalServiceProvider.php© HubspotWebhookServiceProvider.php© JiminnyServiceProvider.php© PlanhatServiceProvider.php© ProphetHandlerServiceProvider.php© PusherServiceProvider.php© AskAnythingPromptService.php© AskAnythingRepository.php X© AutomatedReportsServiceTest.php© AskAnythingPromptDto.phpA console [STAGING]© CoachingFeedbackCoachUserln.phpTx: Autov© AskJiminnyReportsController.phg© AutomatedReportsService.php©) AskAny1gPromptservice lest.php© Search.phpSo jiminny v021 A1 A18 V2 V6 ^class ASkAnythingRepos1torvC) @ueueLoqServiceProvider.php© QueueStatsdServiceProvider.phpC) ResponseMacroServiceProvider.pho© RouteServiceProvider.phpc) SsoServiceProvider.pho@ UtilServiceProvider.ohnC) ViewerGuardServiceProvider.oho> D Queuev Revositories>MAiAutoScoringM Calendarv Merm© AccountRepository.php© ContactRepository.php(e) ContactRo leRepository.ong© CrmConfigurationRepository.php© CrmEntityRepository.php© FieldDataRepository.php© FieldRepository.php© LayoutEntityRepository.php© LayoutRepository.php© LeadRepository.php© OpportunityRepository.php© ProfileRepository.php© RecordTypeFieldValuesRepository.php@ StageRepositorv.php@ SvncBatchRepositorv.php>M GeographyC) ActiveStreamsReoositorv.oho© ActivityCommentRepository.phpC) ActivitvLoaReoositorv.oho©ActivityMessageRepository.phpC) ActivitvMomentRenositorv.nhnl©ActivityProviderRepository.phpC) [EMAIL]"C) ActivitvShareRenositorv [EMAIL](C) A:PromntRenositorv nhn(0 AckAnvthinaDonocitory nhn© AutomatedReportsRepository.php© CallImportRepositorv.phpshed 1 commit to oriain/IV.20676-delete-rend* dreturn collection<lseraskanuthinaPromot›A12 ^ v 182183SELECT ar.id, ar.uuid, ar.mediatype, ar.status, a.typeFROM automated_report_results arJOIN automated_reports a ON a.id = ar.reportidWHERE a.type ='ask_jiminny'LIMIT 10;public function findSharedUsersAndGroupsByPromptId(int $promptId): Collection(...}188 011 usagespublic function findSharedPromptByUser(int $promptid, User Suser): ?UserAskAnythingPromptt...}198SELECT * FROM automated reports where id = 71;SELECT * FROM automated report results where report id = 71:UPDATE automated reports set playbook categories = NULL where id = 68SELECT * FROM automated report results where id = 275;public function findSharedPromptByUserGroup(int $promptId, User Suser): ?UserAskAnythingPrompt{...]192193194* @return Collection<AskAnythingPrompt>SELECT * FROM autorated_reports order by za desc;SELECT * FROMautomated_reportresults order by 1d desc:select * from activity_searches here user_id = 143;Method Jiminny\Repositories\AskAnythingRepository.findPromptsByUserAndTarget AskAnythingRepository ../app/Reposinated report results.* FROM 'automated report resultsiEH→CiG • & Project Files vUsagested renort results', renort id = 'automated renorts'.'1di© AskAnythingPromptService.php© ReorderAskAnythingPromptsRequest.php 32© ReorderAskAnythingPromptsRequest.php 62C) AskAnythingPromptServiceTest.php$prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(->findPromptsByUserAndTarget(Sthis->user(), $this->getTarget())->findPromptsByUserAndTarget(Sthis->user(), Sthis->getTarget())ated_report_results. generated_at IS NOT NULLomated renort results' 'sent at' IS NOT NULU->metnod(TinaPromptsbyuserAnd larger)ated renorts' 'team 1d' = 1.ONTAINS( automated_reports. recipients, 143, '$."users"')->get@->man(function (UserAskAnvthingPrompt SuserPromot) {...})// Remove those prompts that are hidden for the current userSusens0wnedPromotsFiltered = Susens0wnedPromnts-sfilten(function (AskAnvthinaPromnt SusenPrompt 210SELECT * FROM automated_reports where id = 67:SELECT * FROM automated_reports where id = 42;SELECT * FROM users WHERE id = 143; # group 28$defaultNonChangedPrompts = AskAnythingPrompt::where('target', $target)-›whereDoesntHave('userPrompts', function ($query) use (Suser) {...})>whereNullownen id')->aetos213• 214215select * from teams where id = 3143;select * from crm_configurations where id = 500:select * from users where name = 'Integration Account': # 1695SELECT * FROM social accounts WHERE sociable id = 1695:$allPrompts = $defaultNonChangedPrompts->merge(Susers0wnedPromptsFiltered);select * from activities where crm_ configuration id = 39and recording state = 'recorded' and duration > 60and status = 'completed' and actual start time >= '2025-12-01':if ($allPrompts->isNotEmpty@) {$allPrompts->loadCount('automatedReports'):-220222-223SELECT * FROM activities WHERE uuid to bin('458cf915-b914-4000-b083-568763262956') = uuidiselect * from leadsireturn saluprompts._225* dparam arrau<user> Ssharelsers105* aparam arrau<Grouo> SsharebrouosiSELECT * FROM activities WHERE uuid to binc'f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid: # 422003SELECT * FROM activities WHERE id IN (16,422003):SSIECT * SROM activitios whenp status ='failed':Socket fail to connect to host:address=(host=localhost) (port=3306)(tvpe=primarv). Connection refusedectc /I View null reauect (todav 12:17)• suppont Dally • In 3um100% 5• Tue 19 May 14:30:32+0 ..CascadeAsk Jiminny Report MDo you neea sprompt->getHaskeportso in tne DIU?Yes tor the list oath — that's the whole reason for the tlao, current v in det O:1AskAnvthinaPromotService.oho:51-60$promptDtosl] = new AskAnythingPromptDto(Sprompt->gecuuzd),Spromot>aetTitleo.$prompt->getContent(),Sshareusers$prompt->getHasReports(),That's correct and already there. Thanks to the loadCount in the repository. this is now • (1) extra query for the whole list.What about edito?edit() currently doesn't set hasReports, which is fine and intentional:• The newly created prompt (in the isDefaultPrompt branch) is fresh - by definition zero reports.• The edited prompt may or may not have reports attached, but: editing the prompt doesn't add or remove reportassociations, so the trontend already knows the value from when the prompt was loaded in the list. Ine Pul responsedoesn't need to recomoute it.If you want the response to be consistent with create() (which | changed to explicitly send false), you have two options1. Leave edit() as-is — has reports is simply absent from the response. Cleanest semantically: "we didn't touch this:don't trust whatever vou tind here"2. Set hasReports: $newPrompt->getHasReports() — falls back to an exists() query (since loadCount wasn't run onthe sinale promnt). One eytra chean auery ner edit. Keens the API response shape uniform with the list endoointIf vou want ontion 2 here's the chanaeASKAnyrvice.php:152-161return new AskAnythingPromptDto(Snewprompt-geclitlel,SnewPromnt_>aetTaraet)).chlalredcatinc lmmaldsMy recommendation: leave edit alone. The frontend already has the value from the list, and skipping it avoids anunnecessary DB hit on every edit. The create() case got false only because a brand-new prompt is trivially known tohave no renorts — no auerv costwtoall «eAsk anvthing 884-D)<> Code Claude Qnus 4.7 MediumW Windsurf Teamc1-1.UTE.8Po 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58055
|
2042
|
20
|
2026-05-19T11:30:32.736872+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190232736_m1.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingRepository.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV (docker)APP₴82APP (-zslreturn new JsonResponse(['error'$error],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLaterMore>0.(ahlSupport Daily • in 30 m100% <78• Tue 19 May 14:30:32ED→Describe what you are looking forJiminny ...scnicre# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Nikolay Yankov6 0• MessagesAdd canvas+. Direct messages8. Nikolay Yankov% Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomove Todor Stamatov "Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Newl:: AppsS Jira CloudToast1Message Nikolay Yankov...
|
NULL
|
-3557872199200467616
|
NULL
|
click
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV (docker)APP₴82APP (-zslreturn new JsonResponse(['error'$error],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLaterMore>0.(ahlSupport Daily • in 30 m100% <78• Tue 19 May 14:30:32ED→Describe what you are looking forJiminny ...scnicre# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Nikolay Yankov6 0• MessagesAdd canvas+. Direct messages8. Nikolay Yankov% Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomove Todor Stamatov "Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Newl:: AppsS Jira CloudToast1Message Nikolay Yankov...
|
58053
|
NULL
|
NULL
|
NULL
|
|
58058
|
2043
|
21
|
2026-05-19T11:30:36.657754+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190236657_m2.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingRepository.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIewINavicarecodeFV faVsco.js°9 JY-20676-d PhostormVIewINavicarecodeFV faVsco.js°9 JY-20676-delete-report-related-objectsProiect=custom.log= laravel.log4 SF [jiminny@localhost] x4 HS_local [jiminny@localhost]& console [PROD]# console [euy© AskAnythingPromptService.php© AutomatedReportsServiceTest.php© EncryptionServiceProvider.php© EventServiceProvider.php© HubspotJournalServiceProvider.php© HubspotWebhookServiceProvider.php©) Jiminnyservicerrovider.ong© PlanhatServiceProvider.phpc Proonethandlerservicerrovider.ono© AskAnythingPromptDto.phpA console [STAGING]© CoachingFeedbackCoachUserln.phpTx: Autov© AskJiminnyReportsController.phg© AutomatedReportsService.php© Search.phpSo jiminny v021 A1 A18 V2 V6 ^class ASkAnyth1ngRepos1tory(C) @ueueLoqServiceProvider.ohpC) ResponseMacroServiceProvider.oho© RouteServiceProvider.phpc) SsoServiceProvider.oho(C) Uu ServiceProvider.oho(C) ViewerGuardServiceProvider.ohoD Queuev RevositoriesAutoScoringM Calendarv Merm© AccountRepository.phpo contacirepository.ono(c) ContactRolerepository.ong© CrmConfigurationRepository.phpg crmenutykepository.ong© FieldDataRepository.phpgrlelckepository.onp© LayoutEntityRepository.phpLavoutkepositorv.onec) OpportunityRepository.php© ProfileRepository.phpC) RecordTvperieldValuesRepositorv.php© StageRepositorv.phoc) suncbatchrenositorv.ono> M Geography(C) ActiveStreamsRevositorv.oho© ActivityCommentRepository.php(C) ActivitvLoaRenositorv.oho© ActivitvMessageRepositorv.ohp(C) ActivitvMomentRenositorv.ohnl© ActivityProviderRepository.php(C) [EMAIL]"C) ActivitvShareRenositorv nhn* dreturn collection<lseraskanuthinaPromot›A12 ^ v 182183186SELECT ar.id, ar.uuid, ar.mediatype, ar.status, a.typeFROM automated_report_results arJOIN automated_reports a ON a.id = ar.reportidWHERE a.type ='ask_jiminny'LIMIT 10;public function findSharedUsersAndGroupsByPromptId(int $promptId): Collection(...}Trusages188public function findSharedPromptByUser(int $promptid, User Suser): ?UserAskAnythingPromptt...}190191public function findSharedPromptByUserGroup(int $promptId, User Suser): ?UserAskAnythingPrompt{...]192193* @return Collection<AskAnythingPrompt>196public function findPromptsByUserAndTarget(User Suser. AskAnythingPromptTarget Starget): Collectior 198SuserGroupId = Suser->qetGroupIdO:Susers0wnedPrompts = UserAskAnythingPrompt::with( relations: 'prompt')->where(static function (Squery) use (Suser, SuserGroupId): void {...})'promot' function (Builder Sauery) use (Staraet) {...})->orderByRaw( sal: 'TSNULL('order'), 'order' ASC. 'promot id' ASC')201202204205->geto->mapfunction (userAskAnvthinaPromot SuserPromot) 2...0):208209// Remove those prompts that are hidden for the current userSusens0wnedPromotsfiltered = Susens0wnedPromots->filter(function (AskAnvthingPromot SuserPromot210$defaultNonChangedPrompts = AskAnythingPrompt::where('target', $target)->whereDoesntHave('userPromnts' function (Sauerv) use (Suser) {...})>whereNullownen id')->aetos213• 214215$allPrompts = $defaultNonChangedPrompts->merge(Susers0wnedPromptsFiltered):if ($allPrompts->isNotEmpty@){$allPrompts->loadCount('automatedReports'):217219-2202211222-223SELECT * FROM automated reports where id = 71;SELECT * FROM automated report results where report id = 71;UPDATE automated reports set playbook categories = NULL where id = 68SELECT * FROM automated report results where id = 275;SELECT * FROM automated_reports order by 1d desc;SELECT * FROMautomated_reportresults order by 1d desc:select * from activity_searches here user_id = 143;select * fromSELECTautomated report results.* FROM 'automated reportresultsiINNER JOIN 'automated reportsautomated remort results', renort id' =automated reoorts'.'1diautomated renort results' generated at' IS NOT NUILLIautomated_report_results.sent_at IS NOT NULLANDautomated renorts' 'team id = 1AND JSON_CONTAINS( automated_reports . recipients, 143, '$."users"')SELECT * FROM automated_reports where id = 67:SELECT * FROM automated_reports where id = 42;SELECT * FROM users WHERE id = 143; # group 28select * from teams where id = 3143;select * from crm_configurations where id = 500:select * from users where name = 'Integration Account': # 1695SELECT * FROM social accounts WHERE sociable id = 1695:select * from activities where crm_ configuration id = 39and recording state = 'recorded' and duration > 60and status = 'completed' and actual start time >= '2025-12-01':SELECT * FROM activities WHERE uuid to bin('458cf915-6914-4000-b083-568763262956') = uuidiselect * from leads›return saluprompts.© ActivityUploadSettingRepository.php(C) A:PromntRenositorv nhn101102_225SELECT * FROM activities WHERE uuid to bin('{43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid: # 422003SELECT * FROM activities WHERE id IN (16,422003):1oaaWnallalalHealalcialsvelalatSSIECT * SROM activitios whenp status =© AutomatedReportsRepository.php* dparam arrau<user> SsharelsersSocket fail to connect to host:address=(host=localhost) (port=3306)(tvpe=primarv). Connection refused© CallImportRepository.php• suppont Dally • In 3um100% 5• Tue 19 May 14:30:36CascadeAsk Jiminny Report MDo you neea sprompt->getHaskeportso in the DIU?+0 ..Yes tor the list oath — that's the whole reason for the tlao, current v in det O:1AskAnvthinaPromotService.oho:51-60w$promptDtos(] = new AskAnythingPromptDto(Sprompt->gecuuzd),Spromot>aetTitleo.$prompt->getContent(),Sshareusers$prompt->getHasReports(),That's correct and already there. Thanks to the loadCount in the repository. this is now • (1) extra query for the whole list.What about edito?edit() currently doesn't set hasReports, which is fine and intentional:• The newly created prompt (in the isDefaultPrompt branch) is fresh - by definition zero reports.• The edited prompt may or may not have reports attached, but: editing the prompt doesn't add or remove reportassociations, so the trontend already knows the value from when the prompt was loaded in the list. Ine Pul responsedoesn't need to recomoute it.If you want the response to be consistent with create() (which | changed to explicitly send false ). you have two options1. Leave edit() as-is — has reports is simply absent from the response. Cleanest semantically: "we didn't touch this:don't trust whatever vou tind here"2. Set hasReports: $newPrompt->getHasReports() — falls back to an exists() query (since loadCount wasn't run onthe sinale prompt). One eytra chean auery ner edit. Keens the API response shane uniform with the list endnointIf vou want ontion 2 here's the chanaeASKAnyrvice.php:152-161return new AskAnythingPromptDto(Snewprompt-geclitlel,SnewPromnt_>aetTaraet)).chlalredcatinc lmmaldsMy recommendation: leave edit alone. The frontend already has the value from the list, and skipping it avoids anunnecessary DB hit on every edit. The create() case got false only because a brand-new prompt is trivially known tohave no renorts — no auerv costtoall «eAsk anvthing 884-D)<> Code Claude Qnus 4.7 MediumWN Windsurf Teamc51-15 UTE.8Po 4 spaces...
|
NULL
|
3981317909692722944
|
NULL
|
click
|
ocr
|
NULL
|
PhostormVIewINavicarecodeFV faVsco.js°9 JY-20676-d PhostormVIewINavicarecodeFV faVsco.js°9 JY-20676-delete-report-related-objectsProiect=custom.log= laravel.log4 SF [jiminny@localhost] x4 HS_local [jiminny@localhost]& console [PROD]# console [euy© AskAnythingPromptService.php© AutomatedReportsServiceTest.php© EncryptionServiceProvider.php© EventServiceProvider.php© HubspotJournalServiceProvider.php© HubspotWebhookServiceProvider.php©) Jiminnyservicerrovider.ong© PlanhatServiceProvider.phpc Proonethandlerservicerrovider.ono© AskAnythingPromptDto.phpA console [STAGING]© CoachingFeedbackCoachUserln.phpTx: Autov© AskJiminnyReportsController.phg© AutomatedReportsService.php© Search.phpSo jiminny v021 A1 A18 V2 V6 ^class ASkAnyth1ngRepos1tory(C) @ueueLoqServiceProvider.ohpC) ResponseMacroServiceProvider.oho© RouteServiceProvider.phpc) SsoServiceProvider.oho(C) Uu ServiceProvider.oho(C) ViewerGuardServiceProvider.ohoD Queuev RevositoriesAutoScoringM Calendarv Merm© AccountRepository.phpo contacirepository.ono(c) ContactRolerepository.ong© CrmConfigurationRepository.phpg crmenutykepository.ong© FieldDataRepository.phpgrlelckepository.onp© LayoutEntityRepository.phpLavoutkepositorv.onec) OpportunityRepository.php© ProfileRepository.phpC) RecordTvperieldValuesRepositorv.php© StageRepositorv.phoc) suncbatchrenositorv.ono> M Geography(C) ActiveStreamsRevositorv.oho© ActivityCommentRepository.php(C) ActivitvLoaRenositorv.oho© ActivitvMessageRepositorv.ohp(C) ActivitvMomentRenositorv.ohnl© ActivityProviderRepository.php(C) [EMAIL]"C) ActivitvShareRenositorv nhn* dreturn collection<lseraskanuthinaPromot›A12 ^ v 182183186SELECT ar.id, ar.uuid, ar.mediatype, ar.status, a.typeFROM automated_report_results arJOIN automated_reports a ON a.id = ar.reportidWHERE a.type ='ask_jiminny'LIMIT 10;public function findSharedUsersAndGroupsByPromptId(int $promptId): Collection(...}Trusages188public function findSharedPromptByUser(int $promptid, User Suser): ?UserAskAnythingPromptt...}190191public function findSharedPromptByUserGroup(int $promptId, User Suser): ?UserAskAnythingPrompt{...]192193* @return Collection<AskAnythingPrompt>196public function findPromptsByUserAndTarget(User Suser. AskAnythingPromptTarget Starget): Collectior 198SuserGroupId = Suser->qetGroupIdO:Susers0wnedPrompts = UserAskAnythingPrompt::with( relations: 'prompt')->where(static function (Squery) use (Suser, SuserGroupId): void {...})'promot' function (Builder Sauery) use (Staraet) {...})->orderByRaw( sal: 'TSNULL('order'), 'order' ASC. 'promot id' ASC')201202204205->geto->mapfunction (userAskAnvthinaPromot SuserPromot) 2...0):208209// Remove those prompts that are hidden for the current userSusens0wnedPromotsfiltered = Susens0wnedPromots->filter(function (AskAnvthingPromot SuserPromot210$defaultNonChangedPrompts = AskAnythingPrompt::where('target', $target)->whereDoesntHave('userPromnts' function (Sauerv) use (Suser) {...})>whereNullownen id')->aetos213• 214215$allPrompts = $defaultNonChangedPrompts->merge(Susers0wnedPromptsFiltered):if ($allPrompts->isNotEmpty@){$allPrompts->loadCount('automatedReports'):217219-2202211222-223SELECT * FROM automated reports where id = 71;SELECT * FROM automated report results where report id = 71;UPDATE automated reports set playbook categories = NULL where id = 68SELECT * FROM automated report results where id = 275;SELECT * FROM automated_reports order by 1d desc;SELECT * FROMautomated_reportresults order by 1d desc:select * from activity_searches here user_id = 143;select * fromSELECTautomated report results.* FROM 'automated reportresultsiINNER JOIN 'automated reportsautomated remort results', renort id' =automated reoorts'.'1diautomated renort results' generated at' IS NOT NUILLIautomated_report_results.sent_at IS NOT NULLANDautomated renorts' 'team id = 1AND JSON_CONTAINS( automated_reports . recipients, 143, '$."users"')SELECT * FROM automated_reports where id = 67:SELECT * FROM automated_reports where id = 42;SELECT * FROM users WHERE id = 143; # group 28select * from teams where id = 3143;select * from crm_configurations where id = 500:select * from users where name = 'Integration Account': # 1695SELECT * FROM social accounts WHERE sociable id = 1695:select * from activities where crm_ configuration id = 39and recording state = 'recorded' and duration > 60and status = 'completed' and actual start time >= '2025-12-01':SELECT * FROM activities WHERE uuid to bin('458cf915-6914-4000-b083-568763262956') = uuidiselect * from leads›return saluprompts.© ActivityUploadSettingRepository.php(C) A:PromntRenositorv nhn101102_225SELECT * FROM activities WHERE uuid to bin('{43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid: # 422003SELECT * FROM activities WHERE id IN (16,422003):1oaaWnallalalHealalcialsvelalatSSIECT * SROM activitios whenp status =© AutomatedReportsRepository.php* dparam arrau<user> SsharelsersSocket fail to connect to host:address=(host=localhost) (port=3306)(tvpe=primarv). Connection refused© CallImportRepository.php• suppont Dally • In 3um100% 5• Tue 19 May 14:30:36CascadeAsk Jiminny Report MDo you neea sprompt->getHaskeportso in the DIU?+0 ..Yes tor the list oath — that's the whole reason for the tlao, current v in det O:1AskAnvthinaPromotService.oho:51-60w$promptDtos(] = new AskAnythingPromptDto(Sprompt->gecuuzd),Spromot>aetTitleo.$prompt->getContent(),Sshareusers$prompt->getHasReports(),That's correct and already there. Thanks to the loadCount in the repository. this is now • (1) extra query for the whole list.What about edito?edit() currently doesn't set hasReports, which is fine and intentional:• The newly created prompt (in the isDefaultPrompt branch) is fresh - by definition zero reports.• The edited prompt may or may not have reports attached, but: editing the prompt doesn't add or remove reportassociations, so the trontend already knows the value from when the prompt was loaded in the list. Ine Pul responsedoesn't need to recomoute it.If you want the response to be consistent with create() (which | changed to explicitly send false ). you have two options1. Leave edit() as-is — has reports is simply absent from the response. Cleanest semantically: "we didn't touch this:don't trust whatever vou tind here"2. Set hasReports: $newPrompt->getHasReports() — falls back to an exists() query (since loadCount wasn't run onthe sinale prompt). One eytra chean auery ner edit. Keens the API response shane uniform with the list endnointIf vou want ontion 2 here's the chanaeASKAnyrvice.php:152-161return new AskAnythingPromptDto(Snewprompt-geclitlel,SnewPromnt_>aetTaraet)).chlalredcatinc lmmaldsMy recommendation: leave edit alone. The frontend already has the value from the list, and skipping it avoids anunnecessary DB hit on every edit. The create() case got false only because a brand-new prompt is trivially known tohave no renorts — no auerv costtoall «eAsk anvthing 884-D)<> Code Claude Qnus 4.7 MediumWN Windsurf Teamc51-15 UTE.8Po 4 spaces...
|
58056
|
NULL
|
NULL
|
NULL
|
|
58057
|
2042
|
21
|
2026-05-19T11:30:36.666175+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190236666_m1.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingRepository.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV (docker)APP₴82APP (-zslreturn new JsonResponse(['error'$error],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLaterMore>0.(ahlSupport Daily • in 30 m100% <78• Tue 19 May 14:30:36ED→Describe what you are looking forJiminny ...scnicre# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Nikolay Yankov6 0• MessagesAdd canvas+. Direct messages8. Nikolay Yankov% Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomove Todor Stamatov "Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Newl:: AppsS Jira CloudToast1Message Nikolay Yankov...
|
NULL
|
9189720236208599386
|
NULL
|
click
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV (docker)APP₴82APP (-zslreturn new JsonResponse(['error'$error],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLaterMore>0.(ahlSupport Daily • in 30 m100% <78• Tue 19 May 14:30:36ED→Describe what you are looking forJiminny ...scnicre# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Nikolay Yankov6 0• MessagesAdd canvas+. Direct messages8. Nikolay Yankov% Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomove Todor Stamatov "Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Newl:: AppsS Jira CloudToast1Message Nikolay Yankov...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58059
|
2042
|
22
|
2026-05-19T11:30:40.153219+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190240153_m1.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingRepository.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskAnythingPromptServiceTest
Run 'AskAnythingPromptServiceTest'...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects<br/>Some incoming commits are not fetched<br/>","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,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskAnythingPromptServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskAnythingPromptServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4959269709546757893
|
-8637481863596221053
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskAnythingPromptServiceTest
Run 'AskAnythingPromptServiceTest'
SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV (docker)APP₴82APP (-zslreturn new JsonResponse(['error'$error],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLaterMore>0.(ahlSupport Daily • in 30 m100% <7Tue 19 May 14:30:39ED→Describe what you are looking forJiminny ...scnicre# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Nikolay Yankov6 0• MessagesAdd canvas+. Direct messages8. Nikolay Yankov% Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomove Todor Stamatov "Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Newl:: AppsS Jira CloudToast1Message Nikolay Yankov...
|
58057
|
NULL
|
NULL
|
NULL
|
|
58060
|
2042
|
23
|
2026-05-19T11:30:43.878103+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190243878_m1.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingRepository.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV (docker)APP₴82APP (-zslreturn new JsonResponse(['error'$error],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLaterMore>0.(ahlSupport Daily • in 30 m100% <78• Tue 19 May 14:30:43ED→Describe what you are looking forJiminny ...scnicre# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Nikolay Yankov6 0• MessagesAdd canvas+. Direct messages8. Nikolay Yankov% Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomove Todor Stamatov "Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Newl:: AppsS Jira CloudToast1Message Nikolay Yankov...
|
NULL
|
1474354640488604343
|
NULL
|
click
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV (docker)APP₴82APP (-zslreturn new JsonResponse(['error'$error],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLaterMore>0.(ahlSupport Daily • in 30 m100% <78• Tue 19 May 14:30:43ED→Describe what you are looking forJiminny ...scnicre# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Nikolay Yankov6 0• MessagesAdd canvas+. Direct messages8. Nikolay Yankov% Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomove Todor Stamatov "Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Newl:: AppsS Jira CloudToast1Message Nikolay Yankov...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58061
|
2043
|
22
|
2026-05-19T11:30:43.919695+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190243919_m2.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingRepository.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIewINavicarecodeFV faVsco.js°9 JY-20676-d PhostormVIewINavicarecodeFV faVsco.js°9 JY-20676-delete-report-related-objectsProiect© AskAnythingController.php=custom.log= laravel.log4 SF jiminny@localhost] x4 HS_local [jiminny@localhost]& console [PROD]# console [euy© AskAnythingPromptService.php© AutomatedReportsServiceTest.phpA console [STAGING]© CoachingFeedbackCoachUserln.php© EncryptionServiceProvider.php© EventServiceProvider.php© HubspotJournalServiceProvider.php© HubspotWebhookServiceProvider.php© AskAnythingPromptDto.php© AskJiminnyReportsController.phg© AutomatedReportsService.php© Search.phpSo jiminny v021 A1 A18 V2 V6 ^class ASkAnyth1ngRepos1tory©) Jiminnyservicerrovider.ong© PlanhatServiceProvider.phpc Proonethandlerservicerrovider.ono© PusherServiceProvider.php(C) @ueueLoqServiceProvider.ohpC) ResponseMacroServiceProvider.oho© RouteServiceProvider.phpc) SsoServiceProvider.oho(C) Uu ServiceProvider.ohoC) ViewerGuardServiceProvider.ohoD Queuev RevositoriesAutoScoringM Calendarv Merm© AccountRepository.phpo contacirepository.ono(c) ContactRolerepository.ong© CrmConfigurationRepository.phpg crmenutykepository.ong© FieldDataRepository.phpgrlelckepository.onp© LayoutEntityRepository.phpLavoutkepositorv.onec) OpportunityRepository.php© ProfileRepository.phpC) RecordTvperieldValuesRepositorv.php© StageRepositorv.phpc) suncbatchrenositorv.ono> M Geoaraphy(C) ActiveStreamsRevositorv.oho© ActivityCommentRepository.php(C) ActivitvLoaRenositorv.oho© ActivitvMessageRepositorv.ohp(C) ActivitvMomentRenositorv.ohnl©ActivityProviderRepository.php(C) ActivitvRenositorv.nhn@ ActivitvSearchFilterRepository.php"C) ActivitvShareRenositorv nhn* dreturn collection<lseraskanuthinaPromot›A12 ^ v 182183186SELECT ar.id, ar.uuid, ar.media. type, ar.status, a.typeFROM automated_report_results arJOIN automated_reports a ON a.id = ar.reportidWHERE a.type ='ask_jiminny'LIMIT 10;public function findSharedUsersAndGroupsByPromptId(int $promptId): Collection(...}Trusages188public function findSharedPromptByUser(int $promptid, User Suser): ?UserAskAnythingPromptt...}190191public function findSharedPromptByUserGroup(int $promptId, User Suser): ?UserAskAnythingPrompt{...]192193195)* @return Collection<AskAnythingPrompt>196public function findPromptsByUserAndTarget(User Suser. AskAnythingPromptTarget Starget): Collectior 198SuserGroupId = Suser->qetGroupIdO:Susers0wnedPrompts = UserAskAnythingPrompt::with( relations: 'prompt')->where(static function (Squery) use (Suser, SuserGroupId): void {...})201202'promot' function (Builder Sauery) use (Staraet) {...})->orderByRaw( sal: 'TSNULL('order'), 'order' ASC. 'promot id' ASC')->geto->mapfunction (userAskAnvthinaPromot SuserPromot) 2...0):208209// Remove those prompts that are hidden for the current userSusens0wnedPromotsfiltered = Susens0wnedPromots->filter(function (AskAnvthingPromot SuserPromot210$defaultNonChangedPrompts = AskAnythingPrompt::where('target', $target)->whereDoesntHave('userPromnts' function (Sauerv) use (Suser) {...})>whereNullownen id')->aetos213215$allPrompts = $defaultNonChangedPrompts->merge(Susers0wnedPromptsFiltered):if ($allPrompts->isNotEmpty@){$allPrompts->loadCount('automatedReports'):217218219-2202211222223SELECT * FROM automated reports where id = 71;SELECT * FROM automated report results where report id = 71;UPDATE automated reports set playbook categories = NULL where id = 68SELECT * FROM automated report results where id = 275;SELECT * FROM automated_reports order by 1d desc;SELECT * FROMautomated_reportresults order by 1d desc:select * from activity_searches here user_id = 143;select * fromSELECTautomated report results.* FROM 'automated reportresultsiINNER JOIN 'automated reportsautomated remort results', renort id' =automated reoorts'.'1diautomated renort results' generated at' IS NOT NUILLIautomated_report_results.sent_at IS NOT NULLANDautomated renorts' 'team id = 1AND JSON_CONTAINS( automated_reports . recipients, 143, '$."users"')SELECT * FROM automated_reports where id = 67:SELECT * FROM automated_reports where id = 42;SELECT * FROM users WHERE id = 143; # group 28select * from teams where id = 3143;select * from crm_configurations where id = 500:select * from users where name = 'Integration Account': # 1695SELECT * FROM social accounts WHERE sociable id = 1695:select * from activities where crm_ configuration id = 39and recording state = 'recorded' and duration › 60and status = 'completed' and actual start time >= '2025-12-01':SELECT * FROM activities WHERE uuid to bin('458cf915-6914-4000-b083-568763262956') = uuidinotunn SallPromots:select * from leads›© ActivityUploadSettingRepository.php(C) A:PromntRenositorv nhn101_225SELECT * FROM activities WHERE uuid to bin('{43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid: # 422003SELECT * FROM activities WHERE id IN (16,422003):1oaaWnallalalHealalcialsvelalatSSIECT * SROM activitios whenp status =© AutomatedReportsRepository.php* dparam arrau<user> SsharelsersSocket fail to connect to host:address=(host=localhost) (port=3306)(tvpe=primarv). Connection refused© CallImportRepository.php• suppont Dally • In 3um100% 5• Tue 19 May 14:30:43CascadeAsk Jiminny Report MDo you neea sprompt->getHaskeportso in tne DIU?+0 ..Yes tor the list oath — that's the whole reason for the tlao, current v in det O:1AskAnvthinaPromotService.oho:51-60w$promptDtosl] = new AskAnythingPromptDto(Sprompt->gecuuzd),Spromot>aetTitleo.$prompt->getContent(),Sshareusers$prompt->getHasReports(),That's correct and already there. Thanks to the loadCount in the repository. this is now • (1) extra query for the whole list.What about edito?edit() currently doesn't set hasReports, which is fine and intentional:• The newly created prompt (in the isDefaultPrompt branch) is fresh - by definition zero reports.• The edited prompt may or may not have reports attached, but: editing the prompt doesn't add or remove reportassociations, so the trontend already knows the value from when the prompt was loaded in the list. Ine Pul responsedoesn't need to recomoute it.If you want the response to be consistent with create() (which | changed to explicitly send false ). you have two options1. Leave edit() as-is — has reports is simply absent from the response. Cleanest semantically: "we didn't touch this:don't trust whatever vou tind here"2. Set hasReports: $newPrompt->getHasReports() — falls back to an exists() query (since loadCount wasn't run onthe sinale prompt). One eytra chean auery ner edit. Keens the API response shane uniform with the list endnointIf vou want ontion 2 here's the chanaeASKAnyrvice.php:152-161return new AskAnythingPromptDto(Snewprompt-geclitlel,SnewPromnt_>aetTaraet)).chlalredcatinc lmmaldsMy recommendation: leave edit alone. The frontend already has the value from the list, and skipping it avoids anunnecessary DB hit on every edit. The create() case got false only because a brand-new prompt is trivially known tohave no renorts — no auerv costtoall «eAsk anvthina (884-L)<> Code Claude Qnus 4.7 MediumWN Windsurf Toams 00-1UTF.8Po 4 spaces...
|
NULL
|
5610211157195051984
|
NULL
|
click
|
ocr
|
NULL
|
PhostormVIewINavicarecodeFV faVsco.js°9 JY-20676-d PhostormVIewINavicarecodeFV faVsco.js°9 JY-20676-delete-report-related-objectsProiect© AskAnythingController.php=custom.log= laravel.log4 SF jiminny@localhost] x4 HS_local [jiminny@localhost]& console [PROD]# console [euy© AskAnythingPromptService.php© AutomatedReportsServiceTest.phpA console [STAGING]© CoachingFeedbackCoachUserln.php© EncryptionServiceProvider.php© EventServiceProvider.php© HubspotJournalServiceProvider.php© HubspotWebhookServiceProvider.php© AskAnythingPromptDto.php© AskJiminnyReportsController.phg© AutomatedReportsService.php© Search.phpSo jiminny v021 A1 A18 V2 V6 ^class ASkAnyth1ngRepos1tory©) Jiminnyservicerrovider.ong© PlanhatServiceProvider.phpc Proonethandlerservicerrovider.ono© PusherServiceProvider.php(C) @ueueLoqServiceProvider.ohpC) ResponseMacroServiceProvider.oho© RouteServiceProvider.phpc) SsoServiceProvider.oho(C) Uu ServiceProvider.ohoC) ViewerGuardServiceProvider.ohoD Queuev RevositoriesAutoScoringM Calendarv Merm© AccountRepository.phpo contacirepository.ono(c) ContactRolerepository.ong© CrmConfigurationRepository.phpg crmenutykepository.ong© FieldDataRepository.phpgrlelckepository.onp© LayoutEntityRepository.phpLavoutkepositorv.onec) OpportunityRepository.php© ProfileRepository.phpC) RecordTvperieldValuesRepositorv.php© StageRepositorv.phpc) suncbatchrenositorv.ono> M Geoaraphy(C) ActiveStreamsRevositorv.oho© ActivityCommentRepository.php(C) ActivitvLoaRenositorv.oho© ActivitvMessageRepositorv.ohp(C) ActivitvMomentRenositorv.ohnl©ActivityProviderRepository.php(C) ActivitvRenositorv.nhn@ ActivitvSearchFilterRepository.php"C) ActivitvShareRenositorv nhn* dreturn collection<lseraskanuthinaPromot›A12 ^ v 182183186SELECT ar.id, ar.uuid, ar.media. type, ar.status, a.typeFROM automated_report_results arJOIN automated_reports a ON a.id = ar.reportidWHERE a.type ='ask_jiminny'LIMIT 10;public function findSharedUsersAndGroupsByPromptId(int $promptId): Collection(...}Trusages188public function findSharedPromptByUser(int $promptid, User Suser): ?UserAskAnythingPromptt...}190191public function findSharedPromptByUserGroup(int $promptId, User Suser): ?UserAskAnythingPrompt{...]192193195)* @return Collection<AskAnythingPrompt>196public function findPromptsByUserAndTarget(User Suser. AskAnythingPromptTarget Starget): Collectior 198SuserGroupId = Suser->qetGroupIdO:Susers0wnedPrompts = UserAskAnythingPrompt::with( relations: 'prompt')->where(static function (Squery) use (Suser, SuserGroupId): void {...})201202'promot' function (Builder Sauery) use (Staraet) {...})->orderByRaw( sal: 'TSNULL('order'), 'order' ASC. 'promot id' ASC')->geto->mapfunction (userAskAnvthinaPromot SuserPromot) 2...0):208209// Remove those prompts that are hidden for the current userSusens0wnedPromotsfiltered = Susens0wnedPromots->filter(function (AskAnvthingPromot SuserPromot210$defaultNonChangedPrompts = AskAnythingPrompt::where('target', $target)->whereDoesntHave('userPromnts' function (Sauerv) use (Suser) {...})>whereNullownen id')->aetos213215$allPrompts = $defaultNonChangedPrompts->merge(Susers0wnedPromptsFiltered):if ($allPrompts->isNotEmpty@){$allPrompts->loadCount('automatedReports'):217218219-2202211222223SELECT * FROM automated reports where id = 71;SELECT * FROM automated report results where report id = 71;UPDATE automated reports set playbook categories = NULL where id = 68SELECT * FROM automated report results where id = 275;SELECT * FROM automated_reports order by 1d desc;SELECT * FROMautomated_reportresults order by 1d desc:select * from activity_searches here user_id = 143;select * fromSELECTautomated report results.* FROM 'automated reportresultsiINNER JOIN 'automated reportsautomated remort results', renort id' =automated reoorts'.'1diautomated renort results' generated at' IS NOT NUILLIautomated_report_results.sent_at IS NOT NULLANDautomated renorts' 'team id = 1AND JSON_CONTAINS( automated_reports . recipients, 143, '$."users"')SELECT * FROM automated_reports where id = 67:SELECT * FROM automated_reports where id = 42;SELECT * FROM users WHERE id = 143; # group 28select * from teams where id = 3143;select * from crm_configurations where id = 500:select * from users where name = 'Integration Account': # 1695SELECT * FROM social accounts WHERE sociable id = 1695:select * from activities where crm_ configuration id = 39and recording state = 'recorded' and duration › 60and status = 'completed' and actual start time >= '2025-12-01':SELECT * FROM activities WHERE uuid to bin('458cf915-6914-4000-b083-568763262956') = uuidinotunn SallPromots:select * from leads›© ActivityUploadSettingRepository.php(C) A:PromntRenositorv nhn101_225SELECT * FROM activities WHERE uuid to bin('{43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid: # 422003SELECT * FROM activities WHERE id IN (16,422003):1oaaWnallalalHealalcialsvelalatSSIECT * SROM activitios whenp status =© AutomatedReportsRepository.php* dparam arrau<user> SsharelsersSocket fail to connect to host:address=(host=localhost) (port=3306)(tvpe=primarv). Connection refused© CallImportRepository.php• suppont Dally • In 3um100% 5• Tue 19 May 14:30:43CascadeAsk Jiminny Report MDo you neea sprompt->getHaskeportso in tne DIU?+0 ..Yes tor the list oath — that's the whole reason for the tlao, current v in det O:1AskAnvthinaPromotService.oho:51-60w$promptDtosl] = new AskAnythingPromptDto(Sprompt->gecuuzd),Spromot>aetTitleo.$prompt->getContent(),Sshareusers$prompt->getHasReports(),That's correct and already there. Thanks to the loadCount in the repository. this is now • (1) extra query for the whole list.What about edito?edit() currently doesn't set hasReports, which is fine and intentional:• The newly created prompt (in the isDefaultPrompt branch) is fresh - by definition zero reports.• The edited prompt may or may not have reports attached, but: editing the prompt doesn't add or remove reportassociations, so the trontend already knows the value from when the prompt was loaded in the list. Ine Pul responsedoesn't need to recomoute it.If you want the response to be consistent with create() (which | changed to explicitly send false ). you have two options1. Leave edit() as-is — has reports is simply absent from the response. Cleanest semantically: "we didn't touch this:don't trust whatever vou tind here"2. Set hasReports: $newPrompt->getHasReports() — falls back to an exists() query (since loadCount wasn't run onthe sinale prompt). One eytra chean auery ner edit. Keens the API response shane uniform with the list endnointIf vou want ontion 2 here's the chanaeASKAnyrvice.php:152-161return new AskAnythingPromptDto(Snewprompt-geclitlel,SnewPromnt_>aetTaraet)).chlalredcatinc lmmaldsMy recommendation: leave edit alone. The frontend already has the value from the list, and skipping it avoids anunnecessary DB hit on every edit. The create() case got false only because a brand-new prompt is trivially known tohave no renorts — no auerv costtoall «eAsk anvthina (884-L)<> Code Claude Qnus 4.7 MediumWN Windsurf Toams 00-1UTF.8Po 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58062
|
NULL
|
0
|
2026-05-19T11:30:47.219310+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190247219_m2.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingRepository.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8043719072324535154
|
-8628527368849355612
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
PhostormVIewINavicarecode Project: faVsco.js, menu
PhostormVIewINavicarecodeFV faVsco.js°9 JY-20676-delete-report-related-objectsProiect v© AskAnythingController.php=custom.log= laravel.log4 SF jiminny@localhost] x4 HS_local [jiminny@localhost]& console [PROD]# console [euy© AskAnythingPromptService.php© AutomatedReportsServiceTest.phpA console [STAGING]© CoachingFeedbackCoachUserln.php© EncryptionServiceProvider.php© EventServiceProvider.php© HubspotJournalServiceProvider.php© HubspotWebhookServiceProvider.php© AskAnythingPromptDto.php© AskJiminnyReportsController.phg© AutomatedReportsService.php© Search.phpclass ASkAnyth1ngRepos1tory©) Jiminnyservicerrovider.ong© PlanhatServiceProvider.phpc Proonethandlerservicerrovider.ono© PusherServiceProvider.php(C) @ueueLoqServiceProvider.ohpC) ResponseMacroServiceProvider.oho© RouteServiceProvider.phpc) SsoServiceProvider.oho(C) Uu ServiceProvider.ohoC) ViewerGuardServiceProvider.ohoD Queuev RevositoriesAutoScoringM Calendarv Merm© AccountRepository.php(e) ContactRepository.onp(c) ContactRolerepository.ono© CrmConfigurationRepository.php© CrmEntityRepository.php© FieldDataRepository.phpgrlelckepository.onp© LayoutEntityRepository.phpLavoutkepositorv.onec) OpportunityRepository.php© ProfileRepository.phpC) RecordTvperieldValuesRepositorv.pho© StageRepositorv.phpc) suncbatchrenositorv.ono>M Geography(C) ActiveStreamsRevositorv.oho© ActivityCommentRepository.php(C) ActivitvLoaRenositorv.oho© ActivitvMessageRepositorv.ohp(C) ActivitvMomentRenositorv.ohnl©ActivityProviderRepository.php(C) ActivitvRenositorv.nhn@ ActivitvSearchFilterRepository.php"C) ActivitvShareRenositorv nhn* dreturn collection<lseraskanuthinaPromot›A12 ^ v 182183public function findSharedUsersAndGroupsByPromptId(int $promptId): Collection(...}186Trusages188 0public function findSharedPromptByUser(int $promptid, User Suser): ?UserAskAnythingPromptt...}1190191public function findSharedPromptByUserGroup(int $promptId, User Suser): ?UserAskAnythingPrompt{...]192193195)* @return Collection<AskAnythingPrompt>196public function findPromptsByUserAndTarget(User Suser. AskAnythingPromptTarget Starget): Collectior 198SuserGroupId = Suser->qetGroupIdO:Susers0wnedPrompts = UserAskAnythingPrompt::with( relations: 'prompt')->where(static function (Squery) use (Suser, SuserGroupId): void {...})201202'promot' function (Builder Sauery) use (Staraet) {...})->orderByRaw( sal: 'TSNULL('order'), 'order' ASC.'promot id' ASC')->geto->mapfunction (userAskAnvthinaPromot SuserPromot) 2...0):208// Remove those prompts that are hidden for the current userSusens0wnedPromotsfiltered = Susens0wnedPromots->filter(function (AskAnvthingPromot SuserPromot210$defaultNonChangedPrompts = AskAnythingPrompt::where('target', $target)->whereDoesntHave('userPromnts' function (Sauerv) use (Suser) {...})>whereNullownen id')->aetos215$allPrompts = $defaultNonChangedPrompts->merge(Susers0wnedPromptsFiltered):if ($allPrompts->isNotEmpty@){$allPrompts->loadCount('automatedReports'):return SallPrompts:l217218219-220222- 223© ActivityUploadSettingRepository.php(C) A:PromntRenositorv nhn100102_2251oaaWnallalalHealalcialsvelalat© AutomatedReportsRepository.php© CallImportRepository.php* dparam arrau<user> SsharelsersSo jiminny v021 A1 A18 V2 V6 ^SELECT ar.id, ar.uuid, ar.media. type, ar.status, a.typeFROM automated_report_results arJOIN automated_reports a ON a.id = ar.report.idWHERE a.type ='ask_jiminny'LIMIT 10;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 = 68SELECT * FROM automated report results where id = 275;SELECT * FROM automated_reports order by 1d desc;SELECT * FROMautomated_reportresults order by 1d desc:select * from activity_searches here user_id = 143;select * fromSELECTautomated report results.* FROM 'automated reportresultsiINNER JOIN 'automated reportsautomated remort results', renort id' =automated reoorts'.'1diautomated renort results' generated at' IS NOT NUILLIANDautomated_report_results. sent_at IS NOT NULLautomated renorts' 'team id = 1AND JSON_CONTAINS( automated_reports . recipients, 143, '$."users"')SELECT * FROM automated_reports where id = 67:SELECT * FROM automated_reports where id = 42;SELECT * FROM users WHERE id = 143; # group 28select * from teams where id = 3143;select * from crm_configurations where id = 500:select * from users where name = 'Integration Account': # 1695SELECT * FROM social accounts WHERE sociable id = 1695:select * from activities where crm_ configuration id = 39and recording state = 'recorded' and duration > 60and status = 'completed' and actual start time >= '2025-12-01':SELECT * FROM activities WHERE uuid to bin('458cf915-6914-4000-b083-568763262956') = uuidiselect * from leads›SELECT * FROM activities WHERE uuid to bin('{43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid: # 422003SELECT * FROM activities WHERE id IN (16,422003):SSIECT * SROM activitios whenp status =Socket fail to connect to host:address=(host=localhost) (port=3306)(tvpe=primarv). Connection refused• suppont Dally • In 3um100% 5• Tue 19 May 14:30:46CascadeAsk Jiminny Report MDo you neea sprompt->getHaskeportso in the DIU?+0 ..Yes tor the list oath — that's the whole reason for the tlao, current v in det O:1AskAnvthinaPromotService.oho:51-60w$promptDtos(] = new AskAnythingPromptDto(Sprompt->gecuuzd),Spromot>aetTitleo.$prompt->getContent(),Sshareusers$prompt->getHasReports(),That's correct and already there. Thanks to the loadCount in the repository. this is now • (1) extra query for the whole list.What about edito?edit() currently doesn't set hasReports, which is fine and intentional:• The newly created prompt (in the isDefaultPrompt branch) is fresh - by definition zero reports.• The edited prompt may or may not have reports attached, but: editing the prompt doesn't add or remove reportassociations, so the trontend already knows the value from when the prompt was loaded in the list. Ine Pul responsedoesn't need to recomoute it.If you want the response to be consistent with create() (which | changed to explicitly send false ). you have two options1. Leave edit() as-is — has reports is simply absent from the response. Cleanest semantically: "we didn't touch this:don't trust whatever vou tind here"2. Set hasReports: $newPrompt->getHasReports() — falls back to an exists() query (since loadCount wasn't run onthe sinale prompt). One eytra chean auery ner edit. Keens the API response shane uniform with the list endnointIf vou want ontion 2 here's the chanaeASKAnyrvice.php:152-161return new AskAnythingPromptDto(Snewprompt-geclitlel,SnewPromnt_>aetTaraet)).chlalredcatinc lmmaldsMy recommendation: leave edit alone. The frontend already has the value from the list, and skipping it avoids anunnecessary DB hit on every edit. The create() case got false only because a brand-new prompt is trivially known tohave no renorts — no auerv costtoall «eAsk anvthing 884-D)<> Code Claude Qnus 4.7 MediumWN Windsurf Teams100-28UTE.8io 4 spaces...
|
58061
|
NULL
|
NULL
|
NULL
|
|
58063
|
NULL
|
0
|
2026-05-19T11:30:47.258987+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190247258_m1.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingRepository.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8043719072324535154
|
-8628527368849355612
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
SlackFileEditViewGoHistor Project: faVsco.js, menu
SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV (docker)APP₴82APP (-zslreturn new JsonResponse(['error'$error],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLaterMore>0.(ahlSupport Daily • in 30 m100% <78• Tue 19 May 14:30:46ED→Describe what you are looking forJiminny ...scnicre# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Nikolay Yankov6 0• MessagesAdd canvas+. Direct messages8. Nikolay Yankov% Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomove Todor Stamatov "Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Newl:: AppsS Jira CloudToast1Message Nikolay Yankov...
|
58060
|
NULL
|
NULL
|
NULL
|
|
58064
|
2045
|
0
|
2026-05-19T11:31:05.159302+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190265159_m2.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingRepository.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.10405585,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1961873295031593158
|
-1483003541378331465
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
PhostormProiectVIewINavigarecodeKeractorWindowFV faVsco.js°9 JY-20676-delete-report-related-objects© AskAnythingController.php=custom.log= laravel.log4 SF jiminny@localhost] X4 HS_local [jiminny@localhost]© EncryptionServiceProvider.php© EventServiceProvider.php© HubspotJournalServiceProvider.php© HubspotWebhookServiceProvider.php© JiminnyServiceProvider.php© PlanhatServiceProvider.phpc Proonethandlerservicerrovider.ono© PusherServiceProvider.php© AskAnythingPromptService.php© AskAnythingPromptDto.php© AskJiminnyReportsController.phg© AutomatedReportsService.php© AutomatedReportsServiceTest.phpA console [STAGING]© CoachingFeedbackCoachUserln.phpTx: AutovingPromptservice l est.php© Search.phpclass ASkAnyth1ngRepos1toryC) @ueueLoqServiceProvider.php© QueueStatsdServiceProvider.phpC) ResponseMacroServiceProvider.php© RouteServiceProvider.phpc) SsoServiceProvider.pho© UtilServiceProvider.ohnC) ViewerGuardServiceProvider.ohoD Queuev RevositoriesMAiAutoScoringM Calendarv Merm© AccountRepository.php(e) ContactRepository.onp(c) ContactRoleRepository.ono© CrmConfigurationRepository.php© CrmEntityRepository.php© FieldDataRepository.php© FieldRepository.php© LayoutEntityRepository.phpLavoutkepositorv.onec) OpportunityRepository.php© ProfileRepository.php© RecordTypeFieldValuesRepository.php@ StageRepositorv.phpc) suncbatchrenositorv.ono> GeographyC) ActiveStreamsReoositorv.oho© ActivityCommentRepository.phpC) ActivitvLoaReoositorv.oho© ActivitvMessageRepositorv.ohp* dreturn collection<lseraskanuthinaPromot›A12 ^ v 182183SELECT ar.id, ar.uuid, ar.mediatype, ar.status, a.typeFROM automated_report_results arJOIN automated_reports a ON a.id = ar.reportidWHERE a.type ='ask_jiminny'LaNLl 201public function findSharedUsersAndGroupsByPromptId(int $promptId): Collection(...}188 0public function findSharedPromptByUser(int $promptid, User Suser): ?UserAskAnythingPromptt...}198SELECT * FROM automated reports where id = 71;SELECT * FROM automated report results where report id = 71:UPDATE automated reports set playbook categories = NULL where id = 68SELECT * FROM automated report results where id = 275;public function findSharedPromptByUserGroup(int $promptId, User Suser): ?UserAskAnythingPrompt{...]192193194* @return Collection<AskAnythingPrompt>195196SELECT * FROM automated_reports order by 1d desc;SELECT * FROMautomated_reportresults order by 1d desc:select * from activity_searches where user id = 143:public function findPromptsByUserAndTarget(User Suser. AskAnythinqPromptTarget Starget): Collectior198199SELECTautomated report results.* FROM 'automated reportresultsiINNER JOIN 'automated reportsautomated remort results', renort id =automated reoorts-'idiSuserGroupId = Suser->qetGroupIdO:$users0wnedPrompts = UserAskAnythingPrompt::with( relations: 'prompt')->Where static tunction (squery use suser.SuserGroupId): void {...})201202automated_report_results.generated_at IS NOT NULLautomated renort results' sent at' IS NOT NULI'prompt', function (Builder $query) use ($target) {...})->orderByRaw( sal: 'TSNULL('order'). 'order' ASC.'promot id' ASC')ANDautomated renorts'. 'team id = 1AND JSON_CONTAINS( automated_reports . recipients, 143, '$."users"')->geto->manfunction UseraskAnvthinaPromot SuserPromot) 2...0):208// Remove those prompts that are hidden for the current userSusens0wnedPromotsfiltered = Susens0wnedPromnts->filter(function (AskAnvthingPromot SuserPromnt210SELECT * FROM automated_reports where id = 67:SELECT * FROM automated_reports where id = 42;SELECT * FROM users WHERE id = 143; # group 28$defaultNonChangedPrompts = AskAnythingPrompt::where('target', Starget)-›whereDoesntHave('userPrompts', function ($query) use (Suser) {...})>whereNullownen id')->aetos• 214select * from teams where id = 3143;select * from crm_configurations where id = 500:select * from users where name = 'Integration Account': # 1695SELECT * FROM social accounts WHERE sociable id = 1695:colect * from activities where crm configuration id = 39$allPrompts = $defaultNonChangedPrompts->merge(SusersnedPromptsFiltered):Cascade 9e T=lCommand ??lrecording state = 'recorded' and duration > 60and status = 'completed' and actual start time >= '2025-12-01':(C) ActivitvMomentRenositorv.nhnlif ($allPrompts->isNotEmpty0) {-226©ActivityProviderRepository.php$allPrompts->loadCount('automatedReports'):SELECT * FROM activities WHERE uuid to bin('458cf915-6914-4000-b083-568763262956') = uuid:(C) [EMAIL]-223select * from leads:"C) ActivitvShareRenositorv [EMAIL]• Extract Surround / = :_225(C) A:PromntRenositorv nhnSELECT * FROM activities WHERE uuid to binc'f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid: # 422003SELECT * FROM activities WHERE id IN (16.422003)•(0 AckinvthinaDonocitory nhn© AutomatedReportsRepository.php* dparam arrau<user> Ssharelsers© CallImportRepositorv.php105* aparam arrau<crouo> SsharebrouosiSELECT * FROM activities where status = 'failed':Socket fail to connect to host:address=(host=localhost) (port=3306)(tvpe=primarv). Connection refusediched 1 commit to oriain/IV.20676-delete-renoectc /I View null reauect (todav 12:17)"suppont Dally • In zom100% 5• Tue 19 May 14:31:04AskAnythingPromptServiceTest+0 ..& console [PROD]& console [EU]CascadeAsk Jiminny Report MSo jiminny v@AskAnythingRepository-php#L94-100 is ther eany danger in this? Will that modify existign functionality?LGTBl8XLXOAYAsk anvthina (&4-L)+ @ Code Claude Qnus 1.7 MediumW Windsurf Toams 100-28 /212 charc Aline hreake)UTE.8io 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58066
|
2044
|
0
|
2026-05-19T11:31:16.887511+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779190276887_m1.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingRepository.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects<br/>Some incoming commits are not fetched<br/>","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,"on_screen":true,"is_selected":false,"is_expanded":false}]...
|
-6966022010367698874
|
-564708849257816637
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
SlackFileEditViewGoHistoryWindowHelpDOCKER• ₴1DEV (docker)APP₴82APP (-zslreturn new JsonResponse(['error'$error],Response: :HTTP_UNPROCESSABLE_ENTITYenddiffFixed 1 of 5690 files in 71.757 seconds, 60.00 MBmemory usedWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob:Enumerating objects: 158, done.remote: Counting objects: 100% (103/103), done.remote: Compressing objects: 100% (37/37), done.(delta 80), reused 74 (delta 66), pack-reused 55 (from 3)Receiving objects: 100% (158/158), 80.59 KiB | 982.00 KiB/s, done.Resolving deltas: 100% (85/85), completed with 45 local objects.From github.com:jiminny/app02a33811ee..c6ce5e24407ce96cb5fe..6c59f043583906a9238a..2b5e6ea62bJY-20676-delete-report-related-objectsJY-20808-low-priority-indexing-queueJY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-deto5604af40cf..ea3519f0e2* [new branch]switch-eu-assemblyUpdating 02a33811ee..c6ce5е2440app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/src/components/DealInsights/Modals/AskAnythingModal.vuefront-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vuefront-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vuefront-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.jsfront-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.jsfront-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/__tests_/__snapshots__/AskAnythingSettingsDifront-end/src/components/shared/AskAnything/prompts.jsfront-end/src/components/shared/AskAnything/useAskAnything.jsfront-end/yarn.locktests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phpHomeDMsActivityFilesLaterMore>0.(ahlSupport Daily • in 29 m100% <7Tue 19 May 14:31:16ED→Describe what you are looking forJiminny ...scnicrat# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi.... Direct messages8. Nikolay Yankov% Galya Dimitrova% Vasil Vasilev :R. Aneliya Angelovaã. Stefka Stoyanovao Stoyan Tomova Todor Stamatov *Mario Georgievao Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...l:: AppsS Jira CloudToastNikolay Yankov6 0• Messages+Add canvas@ Files/api/v2/user/asToday ~-prompts?target=callто реално промптовете които показваме врепортите като си го сетват са само отпанорамазначи няма как да си изберат такъв промопт отcallзначи всичко трябва да е наредLukas Kovalik 12:44 PMдаNikolay Yankov 1:12 PMима 1 code smellПушнах мои промени и качвам на neptuneLukas Kovalik 1:17 PMпромених message $error = 'This report is missinga saved search or prompt. Edit the report tocomplete the setup before enabling it.;и пушвамNikolay Yankov 1:50 PMдобре, иам коментари от claudeLukas Kovalik 1:53 PMда гледам гиNewNikolay Yankov 2:13 PMКачих клипче тук - добре се държиhttps://github.com/jiminny/app/pull/12098Message Nikolay Yankov+...
|
NULL
|
NULL
|
NULL
|
NULL
|