Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 505 |
|
0.00% |
0 / 10 |
CRAP | |
0.00% |
0 / 1 |
ReportController | |
0.00% |
0 / 505 |
|
0.00% |
0 / 10 |
2862 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
availableCharts | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
6 | |||
chart | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
30 | |||
data | |
0.00% |
0 / 117 |
|
0.00% |
0 / 1 |
42 | |||
company_users_overview | |
0.00% |
0 / 68 |
|
0.00% |
0 / 1 |
12 | |||
company_licenses_overview | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
6 | |||
company_usage_overview | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
2 | |||
export | |
0.00% |
0 / 164 |
|
0.00% |
0 / 1 |
182 | |||
buildBaseMatchQuery | |
0.00% |
0 / 40 |
|
0.00% |
0 / 1 |
210 | |||
getDateRange | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
42 |
1 | <?php |
2 | |
3 | namespace App\Http\Controllers\v2\Company; |
4 | |
5 | use App\Http\Controllers\Controller; |
6 | use App\Http\Models\Admin\AdminUserInvitation; |
7 | use App\Http\Models\Admin\CompanyGroup; |
8 | use App\Http\Models\Admin\CompanyLicenses; |
9 | use App\Http\Models\Auth\Role; |
10 | use App\Http\Models\Auth\User; |
11 | use App\Http\Models\FlyMsgUserDailyUsage; |
12 | use App\Http\Models\Setting; |
13 | use App\Http\Models\UserInfo; |
14 | use App\Http\Requests\v2\Reports\GetReportRequest; |
15 | use App\Http\Services\Admin\Companies\CompanyUsersService; |
16 | use App\Http\Services\Admin\Users\IndividualUsersService; |
17 | use Carbon\Carbon; |
18 | use Illuminate\Http\JsonResponse; |
19 | use Illuminate\Http\Request; |
20 | use Illuminate\Support\Facades\Log; |
21 | use MongoDB\BSON\UTCDateTime; |
22 | use Symfony\Component\HttpFoundation\StreamedResponse; |
23 | |
24 | class 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 | } |