diff --git a/src/Http/Controllers/Admin/StatsController.php b/src/Http/Controllers/Admin/StatsController.php index da3a595..2616db3 100644 --- a/src/Http/Controllers/Admin/StatsController.php +++ b/src/Http/Controllers/Admin/StatsController.php @@ -10,6 +10,7 @@ use EscolaLms\Reports\Http\Requests\Admin\DateRangeStatsRequest; use EscolaLms\Reports\Http\Requests\Admin\ExportCourseStatRequest; use EscolaLms\Reports\Http\Requests\Admin\ExportTopicStatRequest; +use EscolaLms\Reports\Http\Requests\Admin\ImportCoursesStatsRequest; use EscolaLms\Reports\Http\Requests\Admin\TopicStatsRequest; use EscolaLms\Reports\Services\Contracts\StatsServiceContract; use Illuminate\Http\JsonResponse; @@ -56,6 +57,11 @@ public function courseExport(ExportCourseStatRequest $request): BinaryFileRespon return $this->statsService->export($request->getCourse(), $request->getStat()); } + public function courseImport(ImportCoursesStatsRequest $request) + { + $this->statsService->import($request->getCourse(), $request->file('file')); + } + public function topicExport(ExportTopicStatRequest $request): BinaryFileResponse { return $this->statsService->export($request->getTopic(), $request->getStat()); diff --git a/src/Http/Requests/Admin/ImportCoursesStatsRequest.php b/src/Http/Requests/Admin/ImportCoursesStatsRequest.php new file mode 100644 index 0000000..6976dd9 --- /dev/null +++ b/src/Http/Requests/Admin/ImportCoursesStatsRequest.php @@ -0,0 +1,41 @@ +user()->can('viewAny', Report::class) || $this->user()->can('update', $this->getCourse()); + } + + protected function prepareForValidation() + { + $this->merge([ + 'course_id' => $this->route('course_id') + ]); + } + + public function rules() + { + return [ + 'course_id' => ['required', 'integer', Rule::exists((new Course())->getTable(), 'id')], + 'file' => ['required', 'file:xlsx'], + ]; + } + + public function getCourseId(): int + { + return $this->validated()['course_id']; + } + + public function getCourse(): Course + { + return Course::find($this->getCourseId()); + } +} diff --git a/src/Imports/Stats/Course/FinishedTopicsImport.php b/src/Imports/Stats/Course/FinishedTopicsImport.php new file mode 100644 index 0000000..5f376db --- /dev/null +++ b/src/Imports/Stats/Course/FinishedTopicsImport.php @@ -0,0 +1,31 @@ +course = $course; + } + + public function sheets(): array + { + return [ + new FinishedTopicsStatusesSheet($this->course), + new FinishedTopicsSecondsSheet($this->course), + new FinishedTopicsAttemptsSheet($this->course), + ]; + } +} diff --git a/src/Imports/Stats/Course/Sheets/FinishedTopicsAttemptsSheet.php b/src/Imports/Stats/Course/Sheets/FinishedTopicsAttemptsSheet.php new file mode 100644 index 0000000..41dd9d5 --- /dev/null +++ b/src/Imports/Stats/Course/Sheets/FinishedTopicsAttemptsSheet.php @@ -0,0 +1,43 @@ +updateOrCreate([ + 'course_progress_id' => $courseProgress->getKey(), + 'attempt' => 1, + 'seconds' => 0, + ], [ + 'attendance_date' => now(), + ]); + } + + CourseUserAttendance::query()->updateOrCreate([ + 'course_progress_id' => $courseProgress->getKey(), + 'attempt' => $value, + ], [ + 'seconds' => $courseProgress->seconds, + 'attendance_date' => now(), + ]); + + return $courseProgress; + } + + protected function prepareUpdateData($value, CourseProgress $courseProgress = null): array + { + return [ + 'attempts' => $value, + ]; + } +} diff --git a/src/Imports/Stats/Course/Sheets/FinishedTopicsSecondsSheet.php b/src/Imports/Stats/Course/Sheets/FinishedTopicsSecondsSheet.php new file mode 100644 index 0000000..72c6818 --- /dev/null +++ b/src/Imports/Stats/Course/Sheets/FinishedTopicsSecondsSheet.php @@ -0,0 +1,15 @@ + $value, + ]; + } +} diff --git a/src/Imports/Stats/Course/Sheets/FinishedTopicsSheet.php b/src/Imports/Stats/Course/Sheets/FinishedTopicsSheet.php new file mode 100644 index 0000000..7272ff9 --- /dev/null +++ b/src/Imports/Stats/Course/Sheets/FinishedTopicsSheet.php @@ -0,0 +1,61 @@ +course = $course; + $this->prepareTopics(); + } + + public function onRow(Row $row) + { + $user = User::query()->where('email', '=', $row['Email'])->first(); + if ($user) { + foreach ($this->topics as $title => $topic) { + $this->processRow($user, $topic, $row[$title]); + } + } + } + + protected function processRow(User $user, Topic $topic, $value): CourseProgress + { + $coursesProgress = CourseProgress::query() + ->where('user_id', '=', $user->getKey()) + ->where('topic_id', '=', $topic->getKey()) + ->first(); + if ($coursesProgress) { + $coursesProgress->update($this->prepareUpdateData($value, $coursesProgress)); + } else { + $coursesProgress = CourseProgress::create(array_merge([ + 'user_id' => $user->getKey(), + 'topic_id' => $topic->getKey(), + ], $this->prepareUpdateData($value))); + } + + return $coursesProgress; + } + + protected abstract function prepareUpdateData($value, CourseProgress $courseProgress = null): array; + + protected function prepareTopics() + { + $this->course->topics->each(function ($topic) { + $this->topics[(new TopicTitleStrategyContext($topic))->getStrategy()->makeTitle()] = $topic; + }); + } +} diff --git a/src/Imports/Stats/Course/Sheets/FinishedTopicsStatusesSheet.php b/src/Imports/Stats/Course/Sheets/FinishedTopicsStatusesSheet.php new file mode 100644 index 0000000..205e389 --- /dev/null +++ b/src/Imports/Stats/Course/Sheets/FinishedTopicsStatusesSheet.php @@ -0,0 +1,19 @@ + $value, + 'started_at' => $value > 0 ? ($this->course->active_from ?? now()->subDay()) : null, + 'finished_at' => $value === 2 ? ($this->course->active_to ?? now()->subMinutes(random_int(1, 120))) : null, + ]; + } +} diff --git a/src/Services/Contracts/StatsServiceContract.php b/src/Services/Contracts/StatsServiceContract.php index a194bf9..a1c4b0c 100644 --- a/src/Services/Contracts/StatsServiceContract.php +++ b/src/Services/Contracts/StatsServiceContract.php @@ -3,6 +3,7 @@ namespace EscolaLms\Reports\Services\Contracts; use EscolaLms\Reports\Exceptions\ExportNotExistsException; +use Illuminate\Http\UploadedFile; use Symfony\Component\HttpFoundation\BinaryFileResponse; interface StatsServiceContract @@ -14,4 +15,5 @@ public function getAvailableStats($model = null): array; * @throws ExportNotExistsException */ public function export($model, string $stat): BinaryFileResponse; + public function import($model, UploadedFile $file); } diff --git a/src/Services/StatsService.php b/src/Services/StatsService.php index 1d8833c..4caa9c3 100644 --- a/src/Services/StatsService.php +++ b/src/Services/StatsService.php @@ -3,8 +3,10 @@ namespace EscolaLms\Reports\Services; use EscolaLms\Reports\Exceptions\ExportNotExistsException; +use EscolaLms\Reports\Imports\Stats\Course\FinishedTopicsImport; use EscolaLms\Reports\Services\Contracts\StatsServiceContract; use EscolaLms\Reports\Stats\StatsContract; +use Illuminate\Http\UploadedFile; use Illuminate\Support\Str; use Maatwebsite\Excel\Facades\Excel; use Symfony\Component\HttpFoundation\BinaryFileResponse; @@ -65,4 +67,9 @@ public function export($model, string $stat): BinaryFileResponse \Maatwebsite\Excel\Excel::XLSX ); } + + public function import($model, UploadedFile $file) + { + Excel::import(new FinishedTopicsImport($model), $file); + } } diff --git a/src/Stats/Course/CourseUsersAndGroupsStat.php b/src/Stats/Course/CourseUsersAndGroupsStat.php new file mode 100644 index 0000000..51bf24b --- /dev/null +++ b/src/Stats/Course/CourseUsersAndGroupsStat.php @@ -0,0 +1,91 @@ +topicTable = (new Topic())->getTable(); + $this->lessonTable = (new Lesson())->getTable(); + $this->courseTable = (new Course())->getTable(); + $this->courseUserTable = (new CourseUserPivot())->getTable(); + $this->userTable = (new User())->getTable(); + $this->courseProgressTable = (new CourseProgress())->getTable(); + $this->groupTable = (new Group())->getTable(); + $this->courseGroupTable = (new CourseGroupPivot())->getTable(); + $this->userGroupTable = (new GroupUser())->getTable(); + } + + protected function getGroupUsers(): \Illuminate\Support\Collection + { + return $this->formatGroupResult(Topic::query() + ->select( + $this->topicTable . '.id as topic_id', + $this->topicTable . '.title as topic_title', + $this->topicTable . '.topicable_id', + $this->topicTable . '.topicable_type', + $this->userTable . '.id as user_id', + $this->userTable . '.email as user_email', + $this->userTable . '.first_name as user_first_name', + $this->userTable . '.last_name as user_last_name', + $this->courseProgressTable . '.finished_at', + $this->courseProgressTable . '.seconds', + $this->courseProgressTable . '.started_at', + $this->courseProgressTable . '.attempt', + ) + ->with('topicable') + ->join($this->lessonTable, $this->topicTable . '.lesson_id', '=', $this->lessonTable . '.id') + ->join($this->courseTable, $this->lessonTable . '.course_id', '=', $this->courseTable . '.id') + ->where($this->courseTable . '.id', '=', $this->course->getKey()) + ->join($this->courseGroupTable, $this->courseTable . '.id', '=', $this->courseGroupTable . '.course_id') + ->join($this->groupTable, $this->courseGroupTable . '.group_id', '=', $this->groupTable . '.id') + ->join($this->userGroupTable, $this->groupTable . '.id', '=', $this->userGroupTable . '.group_id') + ->join($this->userTable, $this->userGroupTable . '.user_id', '=', $this->userTable . '.id') + ->leftJoin($this->courseProgressTable, fn(JoinClause $join) => $join + ->on($this->courseProgressTable . '.user_id', '=', $this->userTable . '.id') + ->on($this->courseProgressTable . '.topic_id', '=', $this->topicTable . '.id') + ) + ->get()); + } + + private function formatGroupResult(Collection $result): \Illuminate\Support\Collection + { + return $result + ->groupBy('user_email') + ->map(function ($topics, $userEmail) { + $finished = collect($topics)->first(fn ($topic) => $topic->finished_at === null) === null; + return [ + 'id' => $topics[0]->user_id, + 'name' => $topics[0]->user_first_name . ' ' . $topics[0]->user_last_name, + 'email' => $userEmail, + 'finished_at' => $finished ? collect($topics)->max('finished_at') : null, + 'finished' => $finished, + ]; + }); + } +} diff --git a/src/Stats/Course/FinishedCourse.php b/src/Stats/Course/FinishedCourse.php index 3ac5701..271a9b8 100644 --- a/src/Stats/Course/FinishedCourse.php +++ b/src/Stats/Course/FinishedCourse.php @@ -2,12 +2,12 @@ namespace EscolaLms\Reports\Stats\Course; -class FinishedCourse extends AbstractCourseStat +class FinishedCourse extends CourseUsersAndGroupsStat { public function calculate() { $users = $this->course->users()->withPivot('updated_at', 'finished'); - return $users + return array_merge($users ->get() ->map(fn($user) => [ 'id' => $user->id, @@ -17,6 +17,6 @@ public function calculate() 'finished' => $user->pivot->finished, ]) ->values() - ->toArray(); + ->toArray(), $this->getGroupUsers()->values()->toArray()); } } diff --git a/src/Stats/Course/PeopleFinished.php b/src/Stats/Course/PeopleFinished.php index b7a14f0..a51f1e9 100644 --- a/src/Stats/Course/PeopleFinished.php +++ b/src/Stats/Course/PeopleFinished.php @@ -2,10 +2,11 @@ namespace EscolaLms\Reports\Stats\Course; -class PeopleFinished extends AbstractCourseStat +class PeopleFinished extends CourseUsersAndGroupsStat { public function calculate(): int { - return $this->course->users()->wherePivot('finished', '=', true)->count(); + return $this->course->users()->wherePivot('finished', '=', true)->count() + + $this->getGroupUsers()->countBy(fn ($progress) => $progress['finished'])->get(1); } } diff --git a/src/Stats/Course/PeopleStarted.php b/src/Stats/Course/PeopleStarted.php index ab1bddd..729aa52 100644 --- a/src/Stats/Course/PeopleStarted.php +++ b/src/Stats/Course/PeopleStarted.php @@ -2,10 +2,11 @@ namespace EscolaLms\Reports\Stats\Course; -class PeopleStarted extends AbstractCourseStat +class PeopleStarted extends CourseUsersAndGroupsStat { public function calculate(): int { - return $this->course->users()->wherePivot('finished', false)->count(); + return $this->course->users()->wherePivot('finished', false)->count() + + $this->getGroupUsers()->countBy(fn ($progress) => $progress['finished'])->get(0); } } diff --git a/src/Stats/Course/Strategies/DefaultTopicTitleStrategy.php b/src/Stats/Course/Strategies/DefaultTopicTitleStrategy.php index 7b823a5..6d0912c 100644 --- a/src/Stats/Course/Strategies/DefaultTopicTitleStrategy.php +++ b/src/Stats/Course/Strategies/DefaultTopicTitleStrategy.php @@ -15,6 +15,6 @@ public function __construct(Topic $topic) public function makeTitle(): string { - return class_basename($this->topic->topicable_type) . ' # ' . $this->topic->topic_title; + return class_basename($this->topic->topicable_type) . ' # ' . ($this->topic->topic_title ?? $this->topic->title); } } diff --git a/src/Stats/Course/Strategies/H5PTopicTitleStrategy.php b/src/Stats/Course/Strategies/H5PTopicTitleStrategy.php index b45fbda..d337478 100644 --- a/src/Stats/Course/Strategies/H5PTopicTitleStrategy.php +++ b/src/Stats/Course/Strategies/H5PTopicTitleStrategy.php @@ -19,9 +19,9 @@ public function makeTitle(): string $h5pContent = H5PContent::find($this->topic->topicable->value); if (!$h5pContent || !$h5pContent->library) { - return class_basename($this->topic->topicable_type) . ' # ' . $this->topic->topic_title; + return class_basename($this->topic->topicable_type) . ' # ' . ($this->topic->topic_title ?? $this->topic->title); } - return $h5pContent->library->uberName . ' # ' . $this->topic->topic_title; + return $h5pContent->library->uberName . ' # ' . ($this->topic->topic_title ?? $this->topic->title); } } diff --git a/src/routes.php b/src/routes.php index ccddffb..0dc63ba 100644 --- a/src/routes.php +++ b/src/routes.php @@ -15,6 +15,7 @@ Route::get('/available', [StatsController::class, 'available']); Route::get('/course/{course_id}', [StatsController::class, 'course']); Route::get('/course/{course_id}/export', [StatsController::class, 'courseExport']); + Route::post('/course/{course_id}/import', [StatsController::class, 'courseImport']); Route::get('/cart', [StatsController::class, 'cart']); Route::get('/date-range', [StatsController::class, 'dateRange']); Route::get('/topic/{topic_id}', [StatsController::class, 'topic']);