diff --git a/app/Http/Controllers/StudentController.php b/app/Http/Controllers/StudentController.php index 102692b..e8bacec 100644 --- a/app/Http/Controllers/StudentController.php +++ b/app/Http/Controllers/StudentController.php @@ -97,19 +97,34 @@ public static function getStudentScoresForBlok($group, $cohortId, $onlyForUser = // Pre-compute the scores for each feedback moment up to the current week. $feedbackScores = $feedbackmomenten->mapWithKeys(function($fm) use ($scores) { - return [$fm->id => $scores->where('feedbackmoment_id', $fm->id)->pluck('score', 'student_id')]; + // Note that because this plucks the key to the student_id, it will overwrite any duplicate student_id's with + // the last one. This is fine, because we only want the last score for each student. + // TODO: Or do we want the highest score for each student? + return [ + $fm->id => $scores->where('feedbackmoment_id', $fm->id)->mapWithKeys(function($score) { + return [ + $score->student_id => [ + 'score' => $score->score, + 'attempt' => $score->attempt + ] + ]; + })->toArray() + ]; }); $students = $students->map(function($user) use ($blok, $group, $feedbackScores, $totalPointsToGainUntilNow, $totalBpointsToGainUntilNow) { // Calculate total points by summing the highest scores for each feedback moment. $totalPoints = collect($feedbackScores)->map(function ($scores, $fmId) use ($user) { - return $scores[$user['id']] ?? 0; + return $scores[$user['id']]['score'] ?? 0; })->sum(); // Prepare the scores per feedback moment for the student. $feedbackmomentenScores = collect($feedbackScores)->mapWithKeys(function ($scores, $fmId) use ($user) { - return [$fmId => $scores[$user['id']] ?? null]; + return [$fmId => $scores[$user['id']] ?? [ + 'score' => null, + 'attempt' => null + ]]; }); $totalBpoints = DB::table('student_scores_b')->where('student_id', $user['id'])->sum('score'); diff --git a/app/Livewire/Concerns/CanFloodFill.php b/app/Livewire/Concerns/CanFloodFill.php new file mode 100644 index 0000000..4ffc6d2 --- /dev/null +++ b/app/Livewire/Concerns/CanFloodFill.php @@ -0,0 +1,59 @@ +students); + $feedbackmoment = $this->blok->vakken->pluck('feedbackmomenten')->flatten()->firstWhere('id', $feedbackmomentId); + + // Count the students that have a score for this feedbackmoment and wont be affected by the floodfill + $studentsWithScore = 0; + + foreach($this->students as $student) + { + if(isset($student->feedbackmomenten[$feedbackmomentId]['score'])) + $studentsWithScore++; + } + + $count = $studentCount - $studentsWithScore; + + // TODO: Nice message that tells the user nobody will be affected by the floodfill + if ($count == 0) + return; + + $this->floodFillCount = $count; + $this->floodFillSubject = $feedbackmoment; + $this->floodFillValue = $feedbackmoment->points; + } + + public function doFloodFill() + { + foreach($this->students as $key => $student) + { + if(!isset($student->feedbackmomenten[$this->floodFillSubject->id]['score'])) + { + $student->feedbackmomenten[$this->floodFillSubject->id] = [ + 'attempt' => 1, + 'score' => $this->floodFillValue, + ]; + $this->updatedStudents($this->floodFillValue, $key . '.feedbackmomenten.' . $this->floodFillSubject->id . '.score'); + } + } + + $this->cancelFloodFill(); + } + + public function cancelFloodFill() + { + $this->floodFillValue = -1; + $this->floodFillSubject = null; + $this->floodFillCount = null; + } +} diff --git a/app/Livewire/Concerns/CanManageAttempts.php b/app/Livewire/Concerns/CanManageAttempts.php new file mode 100644 index 0000000..996b0c6 --- /dev/null +++ b/app/Livewire/Concerns/CanManageAttempts.php @@ -0,0 +1,91 @@ +students->firstWhere('id', $studentId); + $feedbackmoment = $this->blok->vakken->pluck('feedbackmomenten')->flatten()->firstWhere('id', $feedbackmomentId); + + $this->manageAttempts = StudentScore::where('student_id', $studentId) + ->where('feedbackmoment_id', $feedbackmomentId) + ->get() + ->mapWithKeys(function ($score) { + return [ + $score->id => [ + 'attempt' => $score->attempt, + 'score' => $score->score, + ] + ]; + }) + ->toArray(); + $this->manageAttemptsStudent = $student; + $this->manageAttemptsFeedbackmoment = $feedbackmoment; + } + + public function removeAttempt($scoreId) + { + StudentScore::find($scoreId)->delete(); + unset($this->manageAttempts[$scoreId]); + $this->updateStudentScores(); + } + + public function doManageAttempts() + { + $highestAttempt = 1; + foreach ($this->manageAttempts as $id => $attempt) { + StudentScore::find($id)->update([ + 'attempt' => $attempt['attempt'], + 'score' => $attempt['score'], + ]); + + if ($highestAttempt < $attempt['attempt']) { + $highestAttempt = $attempt['attempt']; + } + } + + if ($this->manageAttemptsNew > -1) { + $updatedScores = [ + [ + 'student_id' => $this->manageAttemptsStudent->id, + 'feedbackmoment_id' => $this->manageAttemptsFeedbackmoment->id, + 'teacher_id' => auth()->user()->id, + 'score' => $this->manageAttemptsNew ?: 0, + 'attempt' => $highestAttempt + 1, + ], + ]; + + StudentScore::updateFeedbackForStudents($updatedScores); + $this->manageAttemptsNew = -1; + } + + $this->cancelManageAttempts(); + } + + public function cancelManageAttempts() + { + $this->manageAttempts = []; + $this->manageAttemptsStudent = null; + $this->manageAttemptsFeedbackmoment = null; + $this->manageAttemptsNew = -1; + } + + public function addNewAttempt() + { + $this->manageAttemptsNew = 0; + } + + public function removeNewAttempt() + { + $this->manageAttemptsNew = -1; + } +} diff --git a/app/Livewire/StudyPointMatrix.php b/app/Livewire/StudyPointMatrix.php index 4591b79..7246498 100644 --- a/app/Livewire/StudyPointMatrix.php +++ b/app/Livewire/StudyPointMatrix.php @@ -3,6 +3,8 @@ namespace App\Livewire; use App\Http\Controllers\StudentController; +use App\Livewire\Concerns\CanFloodFill; +use App\Livewire\Concerns\CanManageAttempts; use App\Models\StudentScore; use App\Models\Group; use App\Traits\SendsNotifications; @@ -14,11 +16,17 @@ class StudyPointMatrix extends Component { use SendsNotifications; + use CanFloodFill; + use CanManageAttempts; public $blok; public $groups; public $blokken = []; public $students; + public $changedStudents = []; + + #[Url(as: 'points', history: true)] + public $showPoints = 'a'; #[Url(as: 'group', history: true)] public $selectedGroupId = -1; @@ -28,10 +36,6 @@ class StudyPointMatrix extends Component public $fbmsActive; public $vakkenActiveB; - public $floodFillValue = -1; - public $floodFillCount; - public $floodFillSubject; - private $selectedCohortId; public function render() @@ -72,6 +76,7 @@ private function updateStudentScores() list($this->blok, $this->fbmsActive, $this->students, $this->vakkenActiveB) = StudentController::getStudentScoresForBlok($group, $selectedCohortId, blokId: $this->selectedBlokId); $this->selectedBlokId = $this->blok->id; + $this->changedStudents = []; } public function updatedSelectedGroupId() @@ -86,121 +91,108 @@ public function updatedSelectedBlokId() $this->updateStudentScores(); } - public function save() - { - $this->updateStudentScores(); - $this->dispatch('study-point-matrix-changed'); - } - - public function startFloodFill($feedbackmomentId) + public function updatedStudents($value, $key) { - $studentCount = count($this->students); - $feedbackmoment = $this->blok->vakken->pluck('feedbackmomenten')->flatten()->firstWhere('id', $feedbackmomentId); - - // Count the students that have a score for this feedbackmoment and wont be affected by the floodfill - $studentsWithScore = 0; + $parts = explode('.', $key); + $studentId = $parts[0]; + $type = $parts[1]; - foreach($this->students as $student) - { - if(isset($student->feedbackmomenten[$feedbackmomentId])) - $studentsWithScore++; + if ($type === 'bPointsOverview') { + $subjectId = $parts[count($parts) - 1]; + } elseif ($type === 'feedbackmomenten') { + $subjectId = $parts[count($parts) - 2]; } - $count = $studentCount - $studentsWithScore; - - // TODO: Nice message that tells the user nobody will be affected by the floodfill - if ($count == 0) - return; - - $this->floodFillCount = $count; - $this->floodFillSubject = $feedbackmoment; - $this->floodFillValue = $feedbackmoment->points; + $this->changedStudents[$studentId] = [ + 'type' => $type, + 'subjectId' => $subjectId, + 'score' => $value, + ]; } - public function doFloodFill() + public function save() { - foreach($this->students as $key => $student) - { - if(!isset($student->feedbackmomenten[$this->floodFillSubject->id])) - { - $student->feedbackmomenten[$this->floodFillSubject->id] = $this->floodFillValue; - $this->updatedStudents($this->floodFillValue, $key . '.feedbackmomenten.' . $this->floodFillSubject->id); + foreach($this->changedStudents as $studentKey => $change) { + $student = $this->students[$studentKey]; + $subjectId = $change['subjectId']; + $value = $change['score']; + + if($change['type'] == 'bPointsOverview') { + $this->savePointsB($student, $subjectId, $value); + } else { + $this->savePointsA($student, $subjectId, $value); } } - $this->cancelFloodFill(); - } - - public function cancelFloodFill() - { - $this->floodFillValue = -1; - $this->floodFillSubject = null; - $this->floodFillCount = null; + $this->changedStudents = []; } - public function updatedStudents($value, $key) + private function savePointsA($student, $feedbackmomentId, $value) { - $parts = explode('.', $key); - $studentKey = $parts[0]; - $student = $this->students[$studentKey]; - - // handle b points update - // todo: maybe make a model out of B points... - if($parts[1] == 'bPointsOverview') { - $subjectId = $parts[count($parts) - 1]; - $row = DB::table('student_scores_b') - ->where('student_id', $student->id) - ->where('subject_id', $subjectId) - ->first(); - - if (!$row) { - DB::table('student_scores_b')->insert([ - 'student_id' => $student->id, - 'subject_id' => $subjectId, - 'score' => $value ?: 0, - 'teacher_id' => auth()->user()->id, - 'created_at' => \Carbon\Carbon::now(), // Not using Eloquent, so need to handle this manually.. - 'updated_at' => \Carbon\Carbon::now(), // Not using Eloquent, so need to handle this manually.. - ]); - return; - } - if ($value == null) { - // If $value is null, delete the row - DB::table('student_scores_b') - ->where('student_id', $student->id) - ->where('subject_id', $subjectId) - ->delete(); - } else { - // If $value is not null, update the score - DB::table('student_scores_b') - ->where('student_id', $student->id) - ->where('subject_id', $subjectId) - ->update([ - 'score' => $value, - 'updated_at' => \Carbon\Carbon::now(), // Not using Eloquent, so need to handle this manually.. - ]); - } - return; - } - - $feedbackmomentId = $parts[count($parts) - 1]; + $attempt = $student->feedbackmomenten[$feedbackmomentId]['attempt']; if($value == null) { - $score = StudentScore::where('student_id', $student->id)->where('feedbackmoment_id', $feedbackmomentId)->first(); + $score = StudentScore::where('student_id', $student->id) + ->where('feedbackmoment_id', $feedbackmomentId) + ->where('attempt', $attempt) + ->first(); $score?->delete(); } else { + $attempt = $attempt ?? 1; $updatedScores = [ [ 'student_id' => $student->id, 'feedbackmoment_id' => $feedbackmomentId, 'teacher_id' => auth()->user()->id, - 'score' => $value ?: 0 + 'score' => $value ?: 0, + 'attempt' => $attempt, ], ]; + StudentScore::updateFeedbackForStudents($updatedScores); + $student->feedbackmomenten[$feedbackmomentId]['attempt'] = $attempt; + } + } + + private function savePointsB($student, $subjectId, $value) + { + // TODO: Make a model out of B points... + $row = DB::table('student_scores_b') + ->where('student_id', $student->id) + ->where('subject_id', $subjectId) + ->first(); + + if (!$row) { + DB::table('student_scores_b')->insert([ + 'student_id' => $student->id, + 'subject_id' => $subjectId, + 'score' => $value ?: 0, + 'teacher_id' => auth()->user()->id, + 'created_at' => \Carbon\Carbon::now(), // Not using Eloquent, so need to handle this manually.. + 'updated_at' => \Carbon\Carbon::now(), // Not using Eloquent, so need to handle this manually.. + ]); + + return; + } + + if ($value == null) { + // If $value is null, delete the row + DB::table('student_scores_b') + ->where('student_id', $student->id) + ->where('subject_id', $subjectId) + ->delete(); + } else { + // If $value is not null, update the score + DB::table('student_scores_b') + ->where('student_id', $student->id) + ->where('subject_id', $subjectId) + ->update([ + 'score' => $value, + 'updated_at' => \Carbon\Carbon::now(), // Not using Eloquent, so need to handle this manually.. + ]); } } } diff --git a/resources/views/components/button-icon.blade.php b/resources/views/components/button-icon.blade.php index eeef742..d8974c6 100644 --- a/resources/views/components/button-icon.blade.php +++ b/resources/views/components/button-icon.blade.php @@ -1,9 +1,12 @@ +@props(['icon', 'compact' => false]) diff --git a/resources/views/components/icon/base.blade.php b/resources/views/components/icon/base.blade.php new file mode 100644 index 0000000..9359d41 --- /dev/null +++ b/resources/views/components/icon/base.blade.php @@ -0,0 +1,9 @@ +@props(['compact' => false]) +class([ + 'h-3 w-3' => $compact, + 'h-6 w-6' => !$compact, + ]); +}}> + {{ $slot }} + diff --git a/resources/views/components/icon/close.blade.php b/resources/views/components/icon/close.blade.php index 3045e26..5f9fe66 100644 --- a/resources/views/components/icon/close.blade.php +++ b/resources/views/components/icon/close.blade.php @@ -1,5 +1,3 @@ -class('w-6 h-6') -}}> + - + diff --git a/resources/views/components/icon/edit.blade.php b/resources/views/components/icon/edit.blade.php index 84a67c5..9298e87 100644 --- a/resources/views/components/icon/edit.blade.php +++ b/resources/views/components/icon/edit.blade.php @@ -1,5 +1,3 @@ -class('w-6 h-6') -}}> + - + diff --git a/resources/views/components/icon/filter.blade.php b/resources/views/components/icon/filter.blade.php index ba9e079..631ce22 100644 --- a/resources/views/components/icon/filter.blade.php +++ b/resources/views/components/icon/filter.blade.php @@ -1,5 +1,3 @@ -class('w-6 h-6') -}}> + - + diff --git a/resources/views/components/icon/loading.blade.php b/resources/views/components/icon/loading.blade.php index 6c40ac9..673a7c1 100644 --- a/resources/views/components/icon/loading.blade.php +++ b/resources/views/components/icon/loading.blade.php @@ -1,5 +1,3 @@ -class('w-6 h-6') -}}> + - + diff --git a/resources/views/components/icon/save.blade.php b/resources/views/components/icon/save.blade.php index 8685721..556f1f4 100644 --- a/resources/views/components/icon/save.blade.php +++ b/resources/views/components/icon/save.blade.php @@ -1,5 +1,3 @@ -class('w-6 h-6') -}}> + - + diff --git a/resources/views/components/icon/stack.blade.php b/resources/views/components/icon/stack.blade.php new file mode 100644 index 0000000..0c9caa9 --- /dev/null +++ b/resources/views/components/icon/stack.blade.php @@ -0,0 +1,3 @@ + + + diff --git a/resources/views/components/icon/tag.blade.php b/resources/views/components/icon/tag.blade.php index 05366f2..a49814e 100644 --- a/resources/views/components/icon/tag.blade.php +++ b/resources/views/components/icon/tag.blade.php @@ -1,6 +1,4 @@ -class('w-6 h-6') -}}> + - + diff --git a/resources/views/components/matrix/a-points-table-body.blade.php b/resources/views/components/matrix/a-points-table-body.blade.php index e9bad7a..ac9a223 100644 --- a/resources/views/components/matrix/a-points-table-body.blade.php +++ b/resources/views/components/matrix/a-points-table-body.blade.php @@ -30,7 +30,8 @@ class="whitespace-nowrap left-0 sticky z-10 flex justify-between items-center {{ @foreach ($vak->feedbackmomenten as $feedbackmoment) @php $columnIndex++; @endphp + + + @endforeach @endforeach @endforeach - \ No newline at end of file + diff --git a/resources/views/components/matrix/b-points-table-body.blade.php b/resources/views/components/matrix/b-points-table-body.blade.php index 52c14f6..0dd354b 100644 --- a/resources/views/components/matrix/b-points-table-body.blade.php +++ b/resources/views/components/matrix/b-points-table-body.blade.php @@ -26,7 +26,7 @@ class="whitespace-nowrap left-0 sticky z-10 flex justify-between items-center {{ @foreach ($this->blok->vakken as $vak) @@ -40,8 +40,7 @@ class="whitespace-nowrap left-0 sticky z-10 flex justify-between items-center {{ }" step="1" min="0" max="2" tabindex="{{ $loop->iteration.$studentIndex }}" - wire:model="students.{{ $key }}.bPointsOverview.{{$vak->uitvoer_id}}" - x-on:input="changesMade['{{ $student->id }} - b{{ $vak->uitvoer_id }}'] = true" --}} + wire:model.live="students.{{ $key }}.bPointsOverview.{{$vak->uitvoer_id}}" x-on:focus="hoverRow = '{{ $student->id }}'; hoverColumn = '{{ $vak->uitvoer_id }}'" /> @endforeach diff --git a/resources/views/livewire/study-point-matrix.blade.php b/resources/views/livewire/study-point-matrix.blade.php index ebd113c..a64b0d7 100644 --- a/resources/views/livewire/study-point-matrix.blade.php +++ b/resources/views/livewire/study-point-matrix.blade.php @@ -1,10 +1,49 @@
+ {{-- Ctrl + S --}} + x-on:keydown.window="if(Object.keys(changesMade).length > 0 && event.ctrlKey && event.key == 's') { event.preventDefault(); $wire.save(); }" + > @php $currentWeek = \App\Models\SchoolWeek::getCurrentWeekNumber() ?? 0; @endphp
@@ -38,6 +77,8 @@ @endif -
-
- + {{-- + We want to show a loading indicator for everything, except for changes on students, so we use this technique: + https://github.com/livewire/livewire/discussions/3855#discussioncomment-6337044 + --}} +
+
+
+ +
@@ -94,11 +141,55 @@ class="absolute -top-1 -right-1"> 'feedbackmoment' => $floodFillSubject->naam, 'fb_code' => $floodFillSubject->code ]) }} van de {{ $floodFillCount }} studenten in deze klas wilt overschrijven met de waarde {{ $floodFillValue }}?

-

Je kunt deze actie niet ongedaan maken!

Annuleren - Vullen + Vullen + + + @endif + + @if (!empty($manageAttempts)) + + Pogingen van {{ $manageAttemptsStudent->name }} + +

+ Let op! Deze wijzigingen (ook verwijderen) worden direct opgeslagen. +

+ +

Dit @if (count($manageAttempts) == 1) is de poging @else zijn de pogingen @endif van {{ $manageAttemptsStudent->name }} bij {{ __('":feedbackmoment (:fb_code)"', [ + 'feedbackmoment' => $manageAttemptsFeedbackmoment->naam, + 'fb_code' => $manageAttemptsFeedbackmoment->code + ]) }}:

+ +
+ @foreach ($manageAttempts as $studentScoreId => $studentScore) +
+ Poging {{ $studentScore['attempt'] }}: + + @if ($loop->last) + Verwijderen + @endif +
+ @endforeach + + @if ($manageAttemptsNew > -1) +
+ Nieuwe poging: + + Verwijderen +
+ @else +
+ Nieuwe Poging Toevoegen +
+ @endif +
+ + + Annuleren + Direct opslaan
@endif diff --git a/resources/views/student.blade.php b/resources/views/student.blade.php index 182e147..276e033 100644 --- a/resources/views/student.blade.php +++ b/resources/views/student.blade.php @@ -61,13 +61,13 @@ {{ str_pad($feedbackmoment->week, 2, "0", STR_PAD_LEFT) }} {{ $feedbackmoment->naam }} {{ str_pad($feedbackmoment->points, 2, "0", STR_PAD_LEFT) }} - @if(isset($student->feedbackmomenten[$feedbackmoment->id]) && $fbmsActive->pluck('id')->contains($feedbackmoment->id)) - - {{ $student->feedbackmomenten[$feedbackmoment->id] }} + @if(isset($student->feedbackmomenten[$feedbackmoment->id]['score']) && $fbmsActive->pluck('id')->contains($feedbackmoment->id)) + + {{ $student->feedbackmomenten[$feedbackmoment->id]['score'] }} @elseif(isset($student->feedbackmomenten[$feedbackmoment->id])) - {{ $student->feedbackmomenten[$feedbackmoment->id] }} + {{ $student->feedbackmomenten[$feedbackmoment->id]['score'] }} @elseif($fbmsActive->pluck('id')->contains($feedbackmoment->id))