Skip to content

Commit 78a7a5c

Browse files
authored
feat: Allow viewing your own theory history (#4650)
* update Theory Question Styling * Revert "update Theory Question Styling" This reverts commit 8d80551. * Allow viewing your own theory history * merge main and filament upgrade * laravel v4 fixes * lint + tests * Update style
1 parent 34f47f0 commit 78a7a5c

File tree

8 files changed

+303
-119
lines changed

8 files changed

+303
-119
lines changed

app/Filament/Training/Pages/MyTraining/MyExamHistory.php

Lines changed: 6 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,10 @@
22

33
namespace App\Filament\Training\Pages\MyTraining;
44

5-
use App\Filament\Training\Pages\Exam\ViewExamReport;
6-
use App\Models\Cts\PracticalResult;
7-
use App\Services\Training\ExamHistoryService;
8-
use Filament\Actions\Action;
95
use Filament\Pages\Page;
10-
use Filament\Tables\Columns\TextColumn;
11-
use Filament\Tables\Concerns\InteractsWithTable;
12-
use Filament\Tables\Contracts\HasTable;
13-
use Filament\Tables\Table;
146

15-
class MyExamHistory extends Page implements HasTable
7+
class MyExamHistory extends Page
168
{
17-
use InteractsWithTable;
18-
199
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-document-text';
2010

2111
protected string $view = 'filament.training.pages.my-training.my-exam-history';
@@ -31,31 +21,11 @@ public static function canAccess(): bool
3121
return auth()->user()->can('training.access') ?? false;
3222
}
3323

34-
public function table(Table $table): Table
24+
protected function getHeaderWidgets(): array
3525
{
36-
$examHistoryService = app(ExamHistoryService::class);
37-
$user = auth()->user();
38-
39-
return $table
40-
->query(
41-
PracticalResult::query()
42-
->whereHas('student', fn ($q) => $q->where('cid', $user->id))
43-
->with(['student', 'examBooking'])
44-
)
45-
->columns([
46-
TextColumn::make('examBooking.exam')->label('Exam'),
47-
TextColumn::make('examBooking.position_1')->label('Position'),
48-
TextColumn::make('result')
49-
->getStateUsing(fn ($record) => $record->resultHuman())
50-
->badge()
51-
->color(fn ($state) => $examHistoryService->getResultBadgeColor($state))
52-
->label('Result'),
53-
54-
TextColumn::make('examBooking.start_date')->label('Exam date'),
55-
])
56-
->defaultSort('date', 'desc')
57-
->recordActions([
58-
Action::make('view')->label('View Report')->url(fn ($record) => ViewExamReport::getUrl(['examId' => $record->examid])),
59-
]);
26+
return [
27+
Widgets\MyPracticalExamHistoryTable::class,
28+
Widgets\MyTheoryExamHistoryTable::class,
29+
];
6030
}
6131
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
namespace App\Filament\Training\Pages\MyTraining\Widgets;
4+
5+
use App\Filament\Training\Pages\Exam\ViewExamReport;
6+
use App\Models\Cts\PracticalResult;
7+
use App\Services\Training\ExamHistoryService;
8+
use Carbon\Carbon;
9+
use Filament\Actions\Action;
10+
use Filament\Tables\Columns\TextColumn;
11+
use Filament\Tables\Table;
12+
use Filament\Widgets\TableWidget as BaseWidget;
13+
14+
class MyPracticalExamHistoryTable extends BaseWidget
15+
{
16+
protected static ?string $heading = 'Practical Exam History';
17+
18+
protected int|string|array $columnSpan = 'full';
19+
20+
protected static ?string $id = 'my-practical-exam-history-table';
21+
22+
public function table(Table $table): Table
23+
{
24+
$examHistoryService = app(ExamHistoryService::class);
25+
$user = auth()->user();
26+
27+
return $table
28+
->query(
29+
PracticalResult::query()
30+
->whereHas('student', fn ($q) => $q->where('cid', $user->id))
31+
->with(['student', 'examBooking'])
32+
)
33+
->columns([
34+
TextColumn::make('examBooking.exam')->label('Exam'),
35+
TextColumn::make('examBooking.position_1')->label('Position'),
36+
TextColumn::make('result')
37+
->getStateUsing(fn ($record) => $record->resultHuman())
38+
->badge()
39+
->color(fn ($state) => $examHistoryService->getResultBadgeColor($state))
40+
->label('Result'),
41+
42+
TextColumn::make('examBooking.start_date')->label('Exam date')->formatStateUsing(fn ($state) => Carbon::parse($state)->isoFormat('lll')),
43+
])
44+
->defaultSort('date', 'desc')
45+
->recordActions([
46+
Action::make('view')->label('View Report')->url(fn ($record) => ViewExamReport::getUrl(['examId' => $record->examid])),
47+
]);
48+
}
49+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
namespace App\Filament\Training\Pages\MyTraining\Widgets;
4+
5+
use App\Filament\Training\Support\TheoryExamViewTrait;
6+
use App\Repositories\Cts\TheoryExamResultRepository;
7+
use Filament\Actions\ViewAction;
8+
use Filament\Tables\Columns\TextColumn;
9+
use Filament\Tables\Table;
10+
use Filament\Widgets\TableWidget as BaseWidget;
11+
12+
class MyTheoryExamHistoryTable extends BaseWidget
13+
{
14+
use TheoryExamViewTrait;
15+
16+
protected static ?string $heading = 'Theory Exam History';
17+
18+
protected int|string|array $columnSpan = 'full';
19+
20+
protected static ?string $id = 'my-theory-exam-history-table';
21+
22+
public function table(Table $table): Table
23+
{
24+
$repo = app(TheoryExamResultRepository::class);
25+
$user = auth()->user();
26+
27+
return $table
28+
->query(
29+
$repo->getTheoryExamHistoryQueryForLevels(collect(['s1', 's2', 's3', 'c1']))->whereHas('student', fn ($q) => $q->where('cid', $user->id))
30+
)
31+
->columns([
32+
TextColumn::make('exam_label')->label('Exam'),
33+
TextColumn::make('score')->label('Score')->getStateUsing(fn ($record) => "{$record->correct} / {$record->questions} (".round(($record->correct / $record->questions) * 100).'%)'),
34+
TextColumn::make('result')->getStateUsing(fn ($record) => $record->resultHuman())->badge()->color(fn ($state) => match ($state) {
35+
'Passed' => 'success',
36+
'Failed' => 'danger',
37+
default => 'gray',
38+
})->label('Result'),
39+
TextColumn::make('submitted_time')->label('Exam date')->isoDateTimeFormat('lll'),
40+
])
41+
->defaultSort('submitted_time', 'desc')
42+
->recordActions([
43+
ViewAction::make('view')
44+
->label('View Report')
45+
->icon(null)
46+
->color('primary')
47+
->modalHeading(fn ($record) => (($record->student?->account?->name) ?? 'Unknown')."'s {$record->exam} Theory Exam")
48+
->infolist($this->theoryExamInfoList()),
49+
]);
50+
}
51+
}

app/Filament/Training/Pages/TheoryExam/TheoryExamHistory.php

Lines changed: 4 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,12 @@
33
namespace App\Filament\Training\Pages\TheoryExam;
44

55
use App\Filament\Training\Pages\TheoryExam\Widgets\TheoryExamOverview;
6+
use App\Filament\Training\Support\TheoryExamViewTrait;
67
use App\Repositories\Cts\TheoryExamResultRepository;
7-
use Carbon\Carbon;
88
use Filament\Actions\ViewAction;
99
use Filament\Forms\Components\DatePicker;
1010
use Filament\Forms\Components\Select;
11-
use Filament\Infolists\Components\TextEntry;
1211
use Filament\Pages\Page;
13-
use Filament\Schemas\Components\Fieldset;
14-
use Filament\Schemas\Components\Section;
1512
use Filament\Tables\Columns\TextColumn;
1613
use Filament\Tables\Concerns\InteractsWithTable;
1714
use Filament\Tables\Contracts\HasTable;
@@ -21,6 +18,7 @@
2118
class TheoryExamHistory extends Page implements HasTable
2219
{
2320
use InteractsWithTable;
21+
use TheoryExamViewTrait;
2422

2523
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-document-text';
2624

@@ -41,47 +39,6 @@ protected function getHeaderWidgets(): array
4139
];
4240
}
4341

44-
protected function buildQuestionPlaceholders($record): array
45-
{
46-
47-
if (! $record) {
48-
return [];
49-
}
50-
51-
$answers = $record->answers()->with('question')->get();
52-
53-
return $answers->map(function ($answer, $index) use ($record) {
54-
$number = $index + 1;
55-
$question = $answer->question;
56-
$questionText = $question->question ?? 'Unknown question';
57-
58-
$givenAnswer = $record->getOptionText($question, $answer->answer_given);
59-
$correctAnswer = $record->getOptionText($question, $question->answer ?? null);
60-
61-
$isCorrect = $answer->answer_given == ($question->answer ?? null);
62-
63-
return Fieldset::make("Question {$number}")
64-
->columnSpanFull()
65-
->schema([
66-
TextEntry::make("question_{$number}_text")
67-
->label('Question')
68-
->getStateUsing($questionText),
69-
TextEntry::make("question_{$number}_answer")
70-
->label('Member Answer')
71-
->getStateUsing($givenAnswer),
72-
TextEntry::make("question_{$number}_correct")
73-
->label('Correct Answer')
74-
->getStateUsing($correctAnswer),
75-
TextEntry::make("question_{$number}_status")
76-
->label('Status')
77-
->badge()
78-
->color($isCorrect ? 'success' : 'danger')
79-
->getStateUsing($isCorrect ? 'CORRECT' : 'INCORRECT'),
80-
])
81-
->columns(4);
82-
})->all();
83-
}
84-
8542
public function table(Table $table): Table
8643
{
8744
$userPermissionsTruthTable = [
@@ -99,7 +56,7 @@ public function table(Table $table): Table
9956
return $table->query($query)->columns([
10057
TextColumn::make('student.cid')->label('CID')->searchable(),
10158
TextColumn::make('student.account.name')->label('Name'),
102-
TextColumn::make('exam')->label('Exam'),
59+
TextColumn::make('exam_label')->label('Exam'),
10360
TextColumn::make('result')->getStateUsing(fn ($record) => $record->resultHuman())->badge()->color(fn ($state) => match ($state) {
10461
'Passed' => 'success',
10562
'Failed' => 'danger',
@@ -114,31 +71,7 @@ public function table(Table $table): Table
11471
->color('primary')
11572
->modalHeading(fn ($record) => (($record->student?->account?->name) ?? 'Unknown')."'s {$record->exam} Theory Exam")
11673
->schema([
117-
Fieldset::make('Exam Information')
118-
->columnSpanFull()
119-
->schema([
120-
TextEntry::make('cid')->label('CID')->getStateUsing(fn ($record) => $record->student_id),
121-
122-
TextEntry::make('Name')->label('Name')->getStateUsing(fn ($record) => $record->student?->account?->name ?? 'Unknown'),
123-
124-
TextEntry::make('Exam')->label('Exam')->getStateUsing(fn ($record) => $record->exam),
125-
126-
TextEntry::make('result')->getStateUsing(fn ($record) => $record->resultHuman())->badge()->color(fn ($state) => match ($state) {
127-
'Passed' => 'success',
128-
'Failed' => 'danger',
129-
})->label('Result'),
130-
]),
131-
132-
Fieldset::make('Details')
133-
->columnSpanFull()
134-
->schema([
135-
TextEntry::make('started')->label('Started')->getStateUsing(fn ($record) => Carbon::parse($record->started)->isoFormat('lll')),
136-
TextEntry::make('submitted_time')->label('Submitted Time')->getStateUsing(fn ($record) => $record->submitted_time ? Carbon::parse($record->submitted_time)->isoFormat('lll') : 'N/A'), // Some exams will not be submitted if they run out of time etc
137-
TextEntry::make('score')->label('Score')->getStateUsing(fn ($record) => "{$record->correct} / {$record->questions} (Passmark: {$record->passmark})"),
138-
TextEntry::make('time_mins')->label('Time Limit')->getStateUsing(fn ($record) => "{$record->time_mins} Mins"),
139-
]),
140-
141-
Section::make('Questions')->collapsible()->collapsed()->columnSpanFull()->schema(fn ($record) => $this->buildQuestionPlaceholders($record)),
74+
...$this->theoryExamInfoList(),
14275
]),
14376
])
14477
->filters([
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
namespace App\Filament\Training\Support;
4+
5+
use Carbon\Carbon;
6+
use Filament\Infolists\Components\TextEntry;
7+
use Filament\Schemas\Components\Fieldset;
8+
use Filament\Schemas\Components\Section;
9+
10+
trait TheoryExamViewTrait
11+
{
12+
protected function buildQuestionPlaceholders($record): array
13+
{
14+
15+
if (! $record) {
16+
return [];
17+
}
18+
19+
$answers = $record->answers()->with('question')->get();
20+
21+
return $answers->map(function ($answer, $index) use ($record) {
22+
$number = $index + 1;
23+
$question = $answer->question;
24+
$questionText = $question->question ?? 'Unknown question';
25+
26+
$givenAnswer = $record->getOptionText($question, $answer->answer_given);
27+
$correctAnswer = $record->getOptionText($question, $question->answer ?? null);
28+
29+
$isCorrect = $answer->answer_given == ($question->answer ?? null);
30+
31+
return Fieldset::make("Question {$number}")
32+
->schema([
33+
TextEntry::make("question_{$number}_text")
34+
->label('Question')
35+
->getStateUsing($questionText),
36+
TextEntry::make("question_{$number}_answer")
37+
->label('Member Answer')
38+
->getStateUsing($givenAnswer),
39+
TextEntry::make("question_{$number}_correct")
40+
->label('Correct Answer')
41+
->getStateUsing($correctAnswer),
42+
TextEntry::make("question_{$number}_status")
43+
->label('Status')
44+
->badge()
45+
->color($isCorrect ? 'success' : 'danger')
46+
->getStateUsing($isCorrect ? 'CORRECT' : 'INCORRECT'),
47+
])
48+
->columns(4);
49+
})->all();
50+
}
51+
52+
protected function theoryExamInfoList(): array
53+
{
54+
return [
55+
Fieldset::make('Exam Information')
56+
->columnSpanFull()
57+
->schema([
58+
TextEntry::make('cid')->label('CID')->getStateUsing(fn ($record) => $record->student_id),
59+
60+
TextEntry::make('Name')->label('Name')->getStateUsing(fn ($record) => $record->student?->account?->name ?? 'Unknown'),
61+
62+
TextEntry::make('Exam')->label('Exam')->getStateUsing(fn ($record) => $record->exam_label),
63+
64+
TextEntry::make('result')->getStateUsing(fn ($record) => $record->resultHuman())->badge()->color(fn ($state) => match ($state) {
65+
'Passed' => 'success',
66+
'Failed' => 'danger',
67+
})->label('Result'),
68+
]),
69+
70+
Fieldset::make('Details')
71+
->columnSpanFull()
72+
->schema([
73+
TextEntry::make('started')->label('Started')->getStateUsing(fn ($record) => Carbon::parse($record->started)->isoFormat('lll')),
74+
TextEntry::make('submitted_time')->label('Submitted Time')->getStateUsing(fn ($record) => $record->submitted_time ? Carbon::parse($record->submitted_time)->isoFormat('lll') : 'N/A'), // Some exams will not be submitted if they run out of time etc
75+
TextEntry::make('score')->label('Score')->getStateUsing(fn ($record) => "{$record->correct} / {$record->questions} (Passmark: {$record->passmark})"),
76+
TextEntry::make('time_mins')->label('Time Limit')->getStateUsing(fn ($record) => "{$record->time_mins} Mins"),
77+
]),
78+
79+
Section::make('Questions')->collapsible()->collapsed()->columnSpanFull()->schema(fn ($record) => $this->buildQuestionPlaceholders($record)),
80+
];
81+
}
82+
}

app/Models/Cts/TheoryResult.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,15 @@ public function getOptionText($question, $optionNumber): string
7878

7979
return $options[$optionNumber] ?? 'Unknown';
8080
}
81+
82+
public function getExamLabelAttribute(): string
83+
{
84+
return match ($this->exam) {
85+
'S1' => 'OBS',
86+
'S2' => 'TWR',
87+
'S3' => 'APP',
88+
'C1' => 'CTR',
89+
default => $this->exam,
90+
};
91+
}
8192
}
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
<x-filament-panels::page>
2-
{{ $this->table }}
32
</x-filament-panels::page>

0 commit comments

Comments
 (0)