Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 505
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
ReportController
0.00% covered (danger)
0.00%
0 / 505
0.00% covered (danger)
0.00%
0 / 10
2862
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 availableCharts
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
 chart
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
30
 data
0.00% covered (danger)
0.00%
0 / 117
0.00% covered (danger)
0.00%
0 / 1
42
 company_users_overview
0.00% covered (danger)
0.00%
0 / 68
0.00% covered (danger)
0.00%
0 / 1
12
 company_licenses_overview
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
6
 company_usage_overview
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
2
 export
0.00% covered (danger)
0.00%
0 / 164
0.00% covered (danger)
0.00%
0 / 1
182
 buildBaseMatchQuery
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
210
 getDateRange
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2
3namespace App\Http\Controllers\v2\Company;
4
5use App\Http\Controllers\Controller;
6use App\Http\Models\Admin\AdminUserInvitation;
7use App\Http\Models\Admin\CompanyGroup;
8use App\Http\Models\Admin\CompanyLicenses;
9use App\Http\Models\Auth\Role;
10use App\Http\Models\Auth\User;
11use App\Http\Models\FlyMsgUserDailyUsage;
12use App\Http\Models\Setting;
13use App\Http\Models\UserInfo;
14use App\Http\Requests\v2\Reports\GetReportRequest;
15use App\Http\Services\Admin\Companies\CompanyUsersService;
16use App\Http\Services\Admin\Users\IndividualUsersService;
17use Carbon\Carbon;
18use Illuminate\Http\JsonResponse;
19use Illuminate\Http\Request;
20use Illuminate\Support\Facades\Log;
21use MongoDB\BSON\UTCDateTime;
22use Symfony\Component\HttpFoundation\StreamedResponse;
23
24class ReportController extends Controller
25{
26    private array $propertyMap = [
27        'time' => 'time_saved',
28        'cost' => 'cost_savings',
29        'shortcuts_created' => 'flycuts_created',
30        'templates' => 'flyplates_added',
31        'chars' => 'characters_typed',
32        'shortcuts_used' => 'flycut_count',
33        'comments_generated' => 'flyengage_count',
34        'posts_generated' => 'flypost_count',
35        'paragraph_rewrite' => 'paragraph_rewrite_count',
36        'grammar' => 'fly_grammar_actions',
37        'issues_accepted' => 'fly_grammar_accepted',
38        'issues_autocorrect' => 'fly_grammar_autocorrect',
39        'issues_autocomplete' => 'fly_grammar_autocomplete',
40    ];
41
42    public function __construct(
43        private CompanyUsersService $companyUsersService,
44        private IndividualUsersService $individualUsersService
45    ) {}
46
47    public function availableCharts(Request $request, string $type): JsonResponse
48    {
49        if ($type === 'effectiveness') {
50            $charts = [
51                ["value" => "cost", "label" => "Cost Saved ($)"],
52                ["value" => "time", "label" => "Time Saved (hrs)"],
53                ["value" => "shortcuts_created", "label" => "Shortcuts Created"],
54                ["value" => "templates", "label" => "Templates Added"],
55            ];
56        } else {
57            $charts = [
58                ["value" => "chars", "label" => "Characters Typed"],
59                ["value" => "grammar", "label" => "AI Grammar Checker Used"],
60                ["value" => "shortcuts_used", "label" => "Shortcuts Deployed"],
61                ["value" => "comments_generated", "label" => "AI Comment Generator Used"],
62                ["value" => "posts_generated", "label" => "AI Post Generator Used"],
63                ["value" => "paragraph_rewrite", "label" => "AI Paragraph Rewrite Used"],
64            ];
65        }
66        return response()->json(['data' => (array)$charts, 'success' => true], 200);
67    }
68
69    public function chart(GetReportRequest $request, string $type): JsonResponse
70    {
71        $property = $this->propertyMap[$type] ?? $type;
72        $round = in_array($type, ['time', 'cost']) ? 2 : 0;
73
74        $baseMatch = $this->buildBaseMatchQuery($request);
75        [$startDate, $endDate] = $this->getDateRange($request, $baseMatch, FlyMsgUserDailyUsage::class);
76
77        $finalMatch = $baseMatch;
78        $finalMatch['created_at'] = ['$gte' => new UTCDateTime($startDate), '$lte' => new UTCDateTime($endDate)];
79
80        $pipeline = [
81            ['$match' => $finalMatch],
82            ['$group' => ['_id' => ['$dateToString' => ['format' => '%Y-%m', 'date' => '$created_at']], 'totalValue' => ['$sum' => '$' . $property]]],
83            ['$sort' => ['_id' => 1]],
84            ['$project' => ['_id' => 0, 'month_year' => '$_id', 'total' => ['$round' => ['$totalValue', $round]]]]
85        ];
86
87        try {
88            $results = FlyMsgUserDailyUsage::raw(fn($c) => $c->aggregate($pipeline));
89            $dbData = [];
90            foreach ($results as $result) {
91                $dbData[$result['month_year']] = $result['total'];
92            }
93            $chart = [];
94            $period = new \DatePeriod($startDate->copy()->startOfMonth(), new \DateInterval('P1M'), $endDate->copy()->endOfMonth());
95            foreach ($period as $date) {
96                $monthKey = $date->format('Y-m');
97                $chart[] = ['period' => $date->format('M Y'), 'value' => $dbData[$monthKey] ?? 0];
98            }
99            return response()->json(['data' => ['chart' => $chart], 'success' => true], 200);
100        } catch (\Exception $e) {
101            return response()->json(['success' => false, 'error' => $e->getMessage()], 400);
102        }
103    }
104
105    public function data(GetReportRequest $request, string $type): JsonResponse
106    {
107        $user = $request->user();
108        $companySetting = Setting::where('company_id', $user->company_id)->first();
109        $wageValue = round($companySetting?->wage_per_hour ?? 31.75, 2);
110        $textMap = [
111            'time' => [
112                'label' => 'Time Saved (hrs)',
113                'title' => 'Time Saved (hrs)',
114                'tooltip' => 'This show the total amount of time users saved over the selected period of time.'
115            ],
116            'cost' => [
117                'label' => 'Cost Saved ($)',
118                'title' => 'Cost Saved ($)',
119                'tooltip' => 'This show the total cost savings in USD using the average US knowledge worker wage of $' . $wageValue . '/over the selected period of time.'
120            ],
121            'shortcuts_created' => [
122                'label' => 'Shortcuts Created',
123                'title' => 'Shortcuts Created',
124                'tooltip' => 'Total number of Shortcuts (FlyCuts) created from users over the selected period of time.'
125            ],
126            'templates' => [
127                'label' => 'Templates Added',
128                'title' => 'Templates Added',
129                'tooltip' => 'Total number of Templates (FlyPlates) added to FlyCuts from users over the selected period of time.'
130            ],
131            'chars' => [
132                'label' => 'Characters Typed',
133                'title' => 'Characters Typed',
134                'tooltip' => 'This shows the total amount of characters users typed over the selected period of time.'
135            ],
136            'shortcuts_used' => [
137                'label' => 'Shortcuts Deployed',
138                'title' => 'Shortcuts Deployed',
139                'tooltip' => 'Total number of Shortcuts (FlyCuts) used by users over the selected period of time.'
140            ],
141            'comments_generated' => [
142                'label' => 'AI Comment Generator Used',
143                'title' => 'AI Comment Generator Used',
144                'tooltip' => 'Total number of times that AI Comment Generator Used (FlyEngage) was used over the selected period of time.'
145            ],
146            'posts_generated' => [
147                'label' => 'AI Post Generator Used',
148                'title' => 'AI Post Generator Used',
149                'tooltip' => 'Total number of times that AI Post Generator Used (FlyPosts) was used over the selected period of time.'
150            ],
151            'paragraph_rewrite' => [
152                'label' => 'AI Paragraph Rewrite Used',
153                'title' => 'AI Paragraph Rewrite Used',
154                'tooltip' => 'This shows the total number of times AI Sentence and Paragraph Rewrite was used over the selected period of time.'
155            ],
156            'grammar' => [
157                'label' => 'AI Grammar Checker Used',
158                'title' => 'AI Grammar Checker Used',
159                'tooltip' => 'Total number of times AI Grammar Checker was used by users over the selected period of time.'
160            ],
161            'issues_accepted' => [
162                'label' => 'AI Grammar Accepted',
163                'title' => 'AI Grammar Accepted',
164                'tooltip' => 'This total represents all grammar suggestions accepted by users over the selected period of time.'
165            ],
166            'issues_autocorrect' => [
167                'label' => 'AI Spelling Autocorrected',
168                'title' => 'AI Spelling Autocorrected',
169                'tooltip' => 'This total represents all spelling auto correction over the selected period of time.'
170            ],
171            'issues_autocomplete' => [
172                'label' => 'AI Sentence Autocompleted',
173                'title' => 'AI Sentence Autocompleted',
174                'tooltip' => 'This total represents all sentences auto completed accepted by users over the selected period of time.'
175            ],
176        ];
177
178        $property = $this->propertyMap[$type] ?? $type;
179        $round = in_array($type, ['time', 'cost']) ? 2 : 0;
180        $textData = $textMap[$type] ?? ['label' => ucfirst($type), 'title' => "Total " . ucfirst($type), 'tooltip' => ''];
181        $topUserLimit = 5;
182
183        $baseMatch = $this->buildBaseMatchQuery($request);
184        [$startDate, $endDate] = $this->getDateRange($request, $baseMatch, FlyMsgUserDailyUsage::class);
185
186        $finalMatch = $baseMatch;
187        $finalMatch['created_at'] = ['$gte' => new UTCDateTime($startDate), '$lte' => new UTCDateTime($endDate)];
188
189        try {
190            // --- QUERY 1: Get Summary Statistics (Total and Unique User Count) ---
191            $summaryPipeline = [
192                ['$match' => $finalMatch],
193                ['$group' => ['_id' => '$user_id', 'userTotal' => ['$sum' => '$' . $property]]],
194                ['$group' => ['_id' => null, 'total' => ['$sum' => '$userTotal'], 'unique_users' => ['$sum' => 1]]]
195            ];
196            $summaryResult = FlyMsgUserDailyUsage::raw(fn($c) => $c->aggregate($summaryPipeline))->first();
197
198            // --- QUERY 2: Get Top Users ---
199            $topUsersPipeline = [
200                ['$match' => $finalMatch],
201                ['$group' => ['_id' => '$user_id', 'value' => ['$sum' => '$' . $property]]],
202                ['$sort' => ['value' => -1]],
203                ['$limit' => $topUserLimit],
204                ['$addFields' => ['userIdAsObjectId' => ['$toObjectId' => '$_id']]],
205                ['$lookup' => ['from' => 'users', 'localField' => 'userIdAsObjectId', 'foreignField' => '_id', 'as' => 'userDetails']],
206                ['$unwind' => ['path' => '$userDetails', 'preserveNullAndEmptyArrays' => true]],
207                ['$project' => [
208                    '_id' => 0,
209                    'name' => ['$ifNull' => [['$concat' => ['$userDetails.first_name', ' ', '$userDetails.last_name']], 'Deleted User']],
210                    'avatar' => ['$ifNull' => ['$userDetails.avatar', null]],
211                    'value' => ['$round' => ['$value', $round]],
212                ]]
213            ];
214            $topUsers = FlyMsgUserDailyUsage::raw(fn($c) => $c->aggregate($topUsersPipeline))->toArray();
215
216            $total = $summaryResult->total ?? 0;
217            $uniqueUsers = $summaryResult->unique_users ?? 0;
218            $average = $uniqueUsers > 0 ? ($total / $uniqueUsers) : 0;
219
220            $summaryData = [
221                'total' => number_format(round($total, $round), $round),
222                'average' => round($average, 2) ? number_format($average, 2) : $average,
223                'top' => $topUsers,
224                'label' => $textData['label'],
225                'title' => $textData['title'],
226                'tooltip' => $textData['tooltip'],
227            ];
228
229            if ($type === 'cost') {
230                $summaryData['valuePrefix'] = '$';
231            }
232
233            return response()->json(['data' => $summaryData, 'success' => true], 200);
234        } catch (\Exception $e) {
235            Log::error('Summary data aggregation error: ' . $e->getMessage());
236            return response()->json(['success' => false, 'error' => $e->getMessage()], 400);
237        }
238    }
239
240    public function company_users_overview(GetReportRequest $request): JsonResponse
241    {
242        $baseMatch = $this->buildBaseMatchQuery($request);
243        // Get Carbon dates first.
244        [$carbonStartDate, $carbonEndDate] = $this->getDateRange($request, $baseMatch, UserInfo::class, 'status_date');
245
246        $pipeline = [];
247        if (!empty($baseMatch)) {
248            $pipeline[] = ['$match' => $baseMatch];
249        }
250
251        // Prepare date clauses with UTCDateTime objects to prevent driver errors.
252        $statusDateClause = [];
253        $extensionInstallDateClause = [];
254        $extensionUninstallDateClause = [];
255
256        if ($carbonStartDate) {
257            $utcStartDate = new UTCDateTime($carbonStartDate);
258            $utcEndDate = new UTCDateTime($carbonEndDate);
259
260            $statusDateClause = ['status_date' => ['$gte' => $utcStartDate, '$lte' => $utcEndDate]];
261
262            $extensionInstallDateClause = ['$or' => [
263                ['flymsg_chrome_extension_installed__date_' => ['$gte' => $utcStartDate, '$lte' => $utcEndDate]],
264                ['flymsg_edge_extension_installed__date_' => ['$gte' => $utcStartDate, '$lte' => $utcEndDate]],
265            ]];
266
267            $extensionUninstallDateClause = ['$or' => [
268                ['flymsg_chrome_extension_uninstalled__date_' => ['$gte' => $utcStartDate, '$lte' => $utcEndDate]],
269                ['flymsg_edge_extension_uninstalled__date_' => ['$gte' => $utcStartDate, '$lte' => $utcEndDate]],
270            ]];
271        }
272
273        $pipeline[] = [
274            '$facet' => [
275                'activated_users' => [
276                    ['$match' => array_merge(['status' => 'Active'], $statusDateClause)],
277                    ['$count' => 'count']
278                ],
279                'inactivated_users' => [
280                    ['$match' => array_merge(['status' => ['$nin' => ['Active', 'Invited']]], $statusDateClause)],
281                    ['$count' => 'count']
282                ],
283                'extensions_installed' => [
284                    ['$match' => array_merge(
285                        ['is_any_extension_installed' => true, 'status' => 'Active'],
286                        $extensionInstallDateClause
287                    )],
288                    ['$count' => 'count']
289                ],
290                'extensions_uninstalled' => [
291                    ['$match' => array_merge(
292                        [
293                            'is_any_extension_installed' => false,
294                            'is_any_extension_uninstalled' => true,
295                            'status' => ['$nin' => ['Deleted', 'Deactivated']]
296                        ],
297                        $extensionUninstallDateClause
298                    )],
299                    ['$count' => 'count']
300                ]
301            ]
302        ];
303
304        $pipeline[] = [
305            '$project' => [
306                'activated_users' => ['$ifNull' => [['$arrayElemAt' => ['$activated_users.count', 0]], 0]],
307                'inactivated_users' => ['$ifNull' => [['$arrayElemAt' => ['$inactivated_users.count', 0]], 0]],
308                'extensions_installed' => ['$ifNull' => [['$arrayElemAt' => ['$extensions_installed.count', 0]], 0]],
309                'extensions_uninstalled' => ['$ifNull' => [['$arrayElemAt' => ['$extensions_uninstalled.count', 0]], 0]],
310            ]
311        ];
312
313        $result = UserInfo::raw(fn($c) => $c->aggregate($pipeline))->first();
314        return response()->json([
315            'data' => [
316                ['amount' => round($result->activated_users ?? 0), 'title' => "Activated Users", 'tooltip' => "Total number of users activated."],
317                ['amount' => round($result->extensions_installed ?? 0), 'title' => "Extension Installed", 'tooltip' => "Total number of activated users with the extension installed."],
318                ['amount' => round($result->inactivated_users ?? 0), 'title' => "Deactivated Users", 'tooltip' => "Total number of users deactivated."],
319                ['amount' => round($result->extensions_uninstalled ?? 0), 'title' => "Extension Uninstalled", 'tooltip' => "Total number of extensions uninstalled."]
320            ],
321            'success' => true
322        ]);
323    }
324
325    public function company_licenses_overview(GetReportRequest $request): JsonResponse
326    {
327        $baseMatch = $this->buildBaseMatchQuery($request);
328        $companyId = $baseMatch['company_id']['$in'][0] ?? $request->user()->company_id;
329
330        $company_license = CompanyLicenses::where("company_id", $companyId)->active()->first();
331        $totalLicenses = $company_license->total_number_of_licenses_available ?? 0;
332        $totalAssignedUserLicenses = User::where("company_id", $companyId)->where('status', 'Active')->count();
333        $totalAssignedInvitationLicenses = AdminUserInvitation::where("company_id", $companyId)->count();
334
335        $pipeline = [];
336
337        if (!empty($baseMatch)) {
338            $pipeline[] = ['$match' => $baseMatch];
339        }
340
341        $pipeline = array_merge($pipeline, [
342            ['$facet' => [
343                'activated_licenses' => [['$match' => ['status' => 'Active']], ['$count' => 'count']],
344                'invited_licenses' => [['$match' => ['status' => 'Invited']], ['$count' => 'count']],
345                'extensions_installed' => [['$match' => ['status' => 'Active', 'is_any_extension_installed' => true]], ['$count' => 'count']]
346            ]],
347            ['$project' => [
348                'activated_licenses' => ['$ifNull' => [['$arrayElemAt' => ['$activated_licenses.count', 0]], 0]],
349                'invited_licenses' => ['$ifNull' => [['$arrayElemAt' => ['$invited_licenses.count', 0]], 0]],
350                'extensions_installed' => ['$ifNull' => [['$arrayElemAt' => ['$extensions_installed.count', 0]], 0]]
351            ]]
352        ]);
353        $result = UserInfo::raw(fn($c) => $c->aggregate($pipeline))->first();
354        $totalActivatedLicenses = $result->activated_licenses ?? 0;
355        $totalInvitedLicenses = $result->invited_licenses ?? 0;
356
357        return response()->json(['data' => [
358            'licenses' => $totalLicenses,
359            'remaining_licenses' => $totalLicenses - $totalAssignedUserLicenses - $totalAssignedInvitationLicenses,
360            'activated_licenses' => $totalActivatedLicenses,
361            'assigned_licenses' => $totalActivatedLicenses + $totalInvitedLicenses,
362            'invitations' => $totalInvitedLicenses,
363            'extensions_installed' => $result->extensions_installed ?? 0
364        ]]);
365    }
366
367    public function company_usage_overview(GetReportRequest $request): JsonResponse
368    {
369        $baseMatch = $this->buildBaseMatchQuery($request);
370        $companyId = $baseMatch['company_id']['$in'][0] ?? $request->user()->company_id;
371        [$startDate, $endDate] = $this->getDateRange($request, $baseMatch, FlyMsgUserDailyUsage::class);
372
373        $finalMatch = $baseMatch;
374        $finalMatch['created_at'] = ['$gte' => new UTCDateTime($startDate), '$lte' => new UTCDateTime($endDate)];
375
376        $pipeline = [
377            ['$match' => $finalMatch],
378            ['$group' => [
379                '_id' => null,
380                'cost' => ['$sum' => '$cost_savings'],
381                'time' => ['$sum' => '$time_saved'],
382                'chars' => ['$sum' => '$characters_typed'],
383                'uniqueUserIds' => ['$addToSet' => '$user_id'],
384            ]],
385            ['$project' => [
386                '_id' => 0,
387                'cost' => 1,
388                'time' => 1,
389                'chars' => 1,
390                'total_users' => ['$size' => '$uniqueUserIds']
391            ]]
392        ];
393
394        $result = FlyMsgUserDailyUsage::raw(fn($c) => $c->aggregate($pipeline))->first();
395        $companySetting = Setting::where('company_id', $companyId)->first();
396        $wageValue = round($companySetting?->wage_per_hour ?? 31.75, 2);
397        $totalUsers = $result->total_users ?? 1;
398
399        return response()->json(['data' => [
400            ['title' => 'ROI Spotlight', 'subtitle' => 'Total Cost Savings ($)', 'tooltip' => "This shows the total cost savings in USD using the average US knowledge worker wage of $" . $wageValue . "/hour.", 'amount' => number_format(round($result->cost ?? 0, 2), 2), 'average' => ['value' => number_format(round(($result->cost ?? 0) / $totalUsers, 2), 2), 'label' => 'Average per User']],
401            ['title' => 'Productivity Spotlight', 'subtitle' => 'Total Time Saved (hrs)', 'tooltip' => 'This shows the total amount of time activated users saved over the selected period of time.', 'amount' => number_format(round($result->time ?? 0, 2), 2), 'average' => ['value' => number_format(round(($result->time ?? 0) / $totalUsers, 2), 2), 'label' => 'Average per User']],
402            ['title' => 'Total Characters Typed', 'subtitle' => 'Characters Typed', 'tooltip' => 'This shows the total amount of characters users typed over the selected period of time.', 'amount' => number_format(round($result->chars ?? 0, 0), 0), 'average' => ['value' => number_format(round(($result->chars ?? 0) / $totalUsers, 0), 2), 'label' => 'Average per User']]
403        ], 'success' => true]);
404    }
405
406    public function export(GetReportRequest $request, string $type): StreamedResponse
407    {
408        $baseMatch = $this->buildBaseMatchQuery($request);
409        $fileName = "report_{$type}_" . now()->format('Y-m-d') . ".csv";
410
411        $callback = function () use ($type, $baseMatch, $request) {
412            $file = fopen('php://output', 'w');
413            fprintf($file, chr(0xEF) . chr(0xBB) . chr(0xBF)); // Add BOM for Excel compatibility
414
415            switch ($type) {
416                case 'effectiveness':
417                    $headers = ['User Name', 'Email', 'License', 'Extension Intalled', 'Cost Savings ($)', 'Time Saved (hrs)', 'Shortcuts Created', 'Templates Added'];
418                    fputcsv($file, $headers);
419
420                    $pipeline = [];
421
422                    if (!empty($baseMatch)) {
423                        $pipeline[] = ['$match' => $baseMatch];
424                    }
425
426                    $pipeline = array_merge($pipeline, [
427                        // ['$addFields' => [
428                        //     'objectUserId' => ['$toObjectId' => '$user_id'],
429                        // ]],
430                        // ['$lookup' => [
431                        //     'from' => 'users',
432                        //     'localField' => 'objectUserId',
433                        //     'foreignField' => '_id',
434                        //     'as' => 'userDetails'
435                        // ]],
436                        // ['$unwind' => '$userDetails'],
437                        ['$project' => [
438                            '_id' => 0,
439                            'full_name' => 1,
440                            'email' => 1,
441                            'plan_name' => 1,
442                            'is_any_extension_installed' => 1,
443                            'cost_savings' => '$total_cost_savings_by_flymsg_by_user',
444                            'time_saved' => '$total_time_saved_by_flymsg_by_user',
445                            'flycuts_created' => '$number_of_flycuts_created_count',
446                            'flyplates_added' => '$number_of_flyplates_in_flycuts_count',
447                        ]]
448                    ]);
449
450                    $data = UserInfo::raw(fn($c) => $c->aggregate($pipeline));
451                    foreach ($data as $row) {
452                        fputcsv($file, [
453                            $row['full_name'],
454                            $row['email'],
455                            $row['plan_name'],
456                            $row['is_any_extension_installed'] ? 'Yes' : 'No',
457                            round($row['cost_savings'] ?? 0, 5),
458                            round($row['time_saved'] ?? 0, 5),
459                            round($row['flycuts_created'] ?? 0, 5),
460                            round($row['flyplates_added'] ?? 0, 5)
461                        ]);
462                    }
463                    break;
464
465                case 'usage':
466                    $headers = [
467                        'User Name',
468                        'Email',
469                        'License',
470                        'Extension Installed',
471                        'Characters Typed',
472                        'AI Grammar Checker Used',
473                        'Shortcuts Deployed',
474                        'AI Comment Generator Used',
475                        'AI Post Generator Used',
476                        'AI Paragraph Rewrite Used'
477                    ];
478                    fputcsv($file, $headers);
479
480                    $pipeline = [];
481
482                    if (!empty($baseMatch)) {
483                        $pipeline[] = ['$match' => $baseMatch];
484                    }
485
486                    $pipeline = array_merge($pipeline, [
487                        // ['$addFields' => [
488                        //     'objectUserId' => ['$toObjectId' => '$user_id'],
489                        // ]],
490                        // ['$lookup' => [
491                        //     'from' => 'users',
492                        //     'localField' => 'objectUserId',
493                        //     'foreignField' => '_id',
494                        //     'as' => 'userDetails'
495                        // ]],
496                        // ['$unwind' => '$userDetails'],
497                        ['$project' => [
498                            '_id' => 0,
499                            'full_name' => 1,
500                            'email' => 1,
501                            'plan_name' => 1,
502                            'is_any_extension_installed' => 1,
503                            'characters_typed' => '$total___of_characters_typed_by_flymsg_by_user',
504                            'fly_grammar_actions' => '$total___of_times_flygrammar_is_used_count',
505                            'flycut_count' => '$total___of_times_flycut_used__count_',
506                            'flyengage_count' => '$total___of_times_flyengage_used__count_',
507                            'flypost_count' => '$total___of_times_flyposts_used__count_',
508                            'paragraph_rewrite_count' => '$total___of_times_paragraph_rewrite_used__count_',
509                        ]]
510                    ]);
511
512                    $data = UserInfo::raw(fn($c) => $c->aggregate($pipeline));
513                    foreach ($data as $row) {
514                        fputcsv(
515                            $file,
516                            [
517                                $row['full_name'],
518                                $row['email'],
519                                $row['plan_name'],
520                                $row['is_any_extension_installed'] ? 'Yes' : 'No',
521                                round($row['characters_typed'] ?? 0, 0),
522                                round($row['fly_grammar_actions'] ?? 0, 0),
523                                round($row['flycut_count'] ?? 0, 0),
524                                round($row['flyengage_count'] ?? 0, 0),
525                                round($row['flypost_count'] ?? 0, 0),
526                                round($row['paragraph_rewrite_count'] ?? 0, 0)
527                            ]
528                        );
529                    }
530                    break;
531
532                case 'overview':
533                    $headers = [
534                        'User Name',
535                        'Email',
536                        'Account Creation',
537                        'Extension Intalled',
538                        'Last Login',
539                        'License',
540                        'Group',
541                        'Subgroup',
542                        'Cost Savings ($)',
543                        'Time Saved (hrs)',
544                        'Characters Typed',
545                        'Shortcuts Created',
546                        'Templates Added',
547                        'Shortcuts Deployed',
548                        'AI Comment Generator Used',
549                        'AI Post Generator Used',
550                        'AI Grammar Checker Used',
551                        'AI Paragraph Rewrite Used',
552                    ];
553                    fputcsv($file, $headers);
554
555                    $pipeline = [];
556
557                    if (!empty($baseMatch)) {
558                        $pipeline[] = ['$match' => $baseMatch];
559                    }
560
561                    $pipeline = array_merge($pipeline, [
562                        ['$project' => [
563                            '_id' => 0,
564                            'full_name' => 1,
565                            'email' => 1,
566                            'user_created_at' => 1,
567                            'is_any_extension_installed' => 1,
568                            'last_login' => 1,
569                            'plan_name' => 1,
570                            'group_name' => 1,
571                            'subgroup_name' => 1,
572                            'total_cost_savings_by_flymsg_by_user' => 1,
573                            'total_time_saved_by_flymsg_by_user' => 1,
574                            'total___of_characters_typed_by_flymsg_by_user' => 1,
575                            'number_of_flycuts_created_count' => 1,
576                            'number_of_flyplates_in_flycuts_count' => 1,
577                            'total___of_times_flycut_used__count_' => 1,
578                            'total___of_times_flyengage_used__count_' => 1,
579                            'total___of_times_flyposts_used__count_' => 1,
580                            'total___of_times_flygrammar_is_used_count' => 1,
581                            'total___of_times_paragraph_rewrite_used__count_' => 1,
582                        ]]
583                    ]);
584
585                    $data = UserInfo::raw(fn($c) => $c->aggregate($pipeline));
586                    foreach ($data as $row) {
587                        fputcsv($file, [
588                            $row['full_name'],
589                            $row['email'],
590                            $row['user_created_at'],
591                            $row['is_any_extension_installed'] ? 'Yes' : 'No',
592                            $row['last_login'],
593                            $row['plan_name'],
594                            $row['group_name'] ?? 'Not Assigned',
595                            $row['subgroup_name'] ?? 'Not Assigned',
596                            round($row['total_cost_savings_by_flymsg_by_user'], 2),
597                            round($row['total_time_saved_by_flymsg_by_user'], 2),
598                            $row['total___of_characters_typed_by_flymsg_by_user'] ?? 0,
599                            $row['number_of_flycuts_created_count'] ?? 0,
600                            $row['number_of_flyplates_in_flycuts_count'] ?? 0,
601                            $row['total___of_times_flycut_used__count_'] ?? 0,
602                            $row['total___of_times_flyengage_used__count_'] ?? 0,
603                            $row['total___of_times_flyposts_used__count_'] ?? 0,
604                            $row['total___of_times_flygrammar_is_used_count'] ?? 0,
605                            $row['total___of_times_paragraph_rewrite_used__count_'] ?? 0,
606                        ]);
607                    }
608                    break;
609            }
610            fclose($file);
611        };
612
613        return new StreamedResponse($callback, 200, [
614            'Content-Type' => 'text/csv',
615            'Content-Disposition' => 'attachment; filename="' . $fileName . '"',
616        ]);
617    }
618
619    private function buildBaseMatchQuery(Request $request): array
620    {
621        $user = $request->user();
622        $roles = $user->roles();
623        $baseMatch = ['user_status' => ['$ne' => 'Invited']];
624        $processIds = fn($ids) => array_values(array_filter(explode(',', $ids ?? '')));
625        $isCmc = $request->input('cmc', false);
626
627        $companyIds = $processIds($request->input('company_ids', $request->input('companyIds')));
628        $userIds = $processIds($request->input('user_ids', $request->input('userIds')));
629        $rawGroupIds = $processIds($request->input('group_ids', $request->input('groupIds')));
630        $rawSubgroupIds = $processIds($request->input('subgroup_ids', $request->input('subgroupIds')));
631        $hasNotAssignedGroup = in_array('-1', $rawGroupIds);
632        $hasNotAssignedSubgroup = in_array('-1', $rawSubgroupIds);
633        $groupIds = array_filter($rawGroupIds, fn($id) => $id !== '-1');
634        $subgroupIds = array_filter($rawSubgroupIds, fn($id) => $id !== '-1');
635
636        if (!$isCmc && empty($companyIds)) {
637            $companyIds = [$user->company_id];
638        }
639
640        if (!in_array(Role::VENGRESO_ADMIN, $roles)) {
641            $companyIds = [$user->company_id];
642            if (!in_array(Role::GLOBAL_ADMIN, $roles)) {
643                $managedGroupIds = CompanyGroup::where('company_id', $user->company_id)
644                    ->where('admins', $user->id)
645                    ->pluck('id')->all();
646                $groupIds = !empty($groupIds) ? array_intersect($groupIds, $managedGroupIds) : $managedGroupIds;
647            }
648        }
649
650        $orConditions = [];
651
652        if (!$isCmc) {
653            $baseMatch['company_id'] = ['$in' => $companyIds];
654        } elseif (!empty($companyIds)) {
655            $orConditions[] = ['company_id' => ['$in' => $companyIds]];
656        }
657
658        if (!empty($userIds)) {
659            $orConditions[] = ['user_id' => ['$in' => $userIds]];
660        }
661        if (!empty($groupIds)) {
662            $orConditions[] = ['group_id' => ['$in' => $groupIds]];
663        }
664        if ($hasNotAssignedGroup) {
665            $orConditions[] = ['group_id' => null];
666        }
667        if (!empty($subgroupIds)) {
668            $orConditions[] = ['subgroup_id' => ['$in' => $subgroupIds]];
669        }
670        if ($hasNotAssignedSubgroup) {
671            $orConditions[] = ['subgroup_id' => null];
672        }
673
674        if (!empty($orConditions)) {
675            $baseMatch['$or'] = $orConditions;
676        }
677        return $baseMatch;
678    }
679
680    private function getDateRange(Request $request, array $baseMatch, string $model, string $dateField = 'created_at'): array
681    {
682        if ($request->filled('from') && $request->filled('to')) {
683            return [Carbon::parse($request->from)->startOfDay(), Carbon::parse($request->to)->endOfDay()];
684        }
685        $datePipeline = [];
686        if (!empty($baseMatch)) $datePipeline[] = ['$match' => $baseMatch];
687        $datePipeline[] = ['$group' => ['_id' => null, 'minDate' => ['$min' => '$' . $dateField], 'maxDate' => ['$max' => '$' . $dateField]]];
688
689        $dateResult = $model::raw(fn($c) => $c->aggregate($datePipeline))->first();
690
691        if ($dateResult && $dateResult->minDate) {
692            $startDate = Carbon::createFromTimestampMs($dateResult->minDate->toDateTime()->getTimestamp() * 1000);
693            $endDate = Carbon::createFromTimestampMs($dateResult->maxDate->toDateTime()->getTimestamp() * 1000);
694        } else {
695            $startDate = Carbon::now()->subYear()->startOfDay();
696            $endDate = Carbon::now()->endOfDay();
697        }
698        return [$startDate, $endDate];
699    }
700}