Merge pull request 'task-5 (Lessons and Scores)' (#5) from feature/task-5 into develop

Reviewed-on: #5
This commit is contained in:
klllst 2024-05-27 18:42:11 +04:00
commit 3068ef2994
28 changed files with 699 additions and 9 deletions

15
app/Enums/ScoreEnum.php Normal file
View File

@ -0,0 +1,15 @@
<?php
namespace App\Enums;
enum ScoreEnum: string
{
case WithoutScore = 'Без оценки';
case One = '1';
case Two = '2';
case Three = '3';
case Four = '4';
case Five = '5';
case Absent = 'Н';
case Sick = 'Б';
}

11
app/Enums/TypeLesson.php Normal file
View File

@ -0,0 +1,11 @@
<?php
namespace App\Enums;
enum TypeLesson: string
{
case Homework = "Домашняя работа";
case Classwork = "Работа в классе";
case TestClass = "Самостоятельная работа";
case ExamClass = "Контрольная работа";
}

View File

@ -0,0 +1,109 @@
<?php
namespace App\Http\Controllers;
use App\Enums\TypeLesson;
use App\Http\Requests\LessonPostRequest;
use App\Models\Grade;
use App\Models\Lesson;
use App\Services\ServiceInterface;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class LessonController extends Controller
{
public function __construct
(
protected ServiceInterface $service,
){
}
public function gradeList(): View
{
return view('grade-lesson.grades-list', [
'grades' => $this->service->getGrades(),
]);
}
/**
* Display a listing of the resource.
*/
public function index(Grade $grade): View
{
return view('grade-lesson.index', [
'lessons' => $this->service->getAll($grade),
'grade' => $grade,
'subjects' => $grade->subjects,
]);
}
/**
* Show the form for creating a new resource.
*/
public function create(Grade $grade): View
{
return view('grade-lesson.create', [
'types' => TypeLesson::cases(),
'grade' => $grade,
]);
}
/**
* Store a newly created resource in storage.
*/
public function store(LessonPostRequest $request, Grade $grade): RedirectResponse
{
return redirect()->route(
'grades.lessons.show', [
$grade,
$this->service->create($request->validated())
]
);
}
/**
* Display the specified resource.
*/
public function show(Grade $grade, Lesson $lesson): View
{
return view('grade-lesson.show', [
'lesson' => $lesson,
'grade' => $grade,
]);
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Grade $grade, Lesson $lesson): View
{
return view('grade-lesson.edit', [
'lesson' => $lesson,
'grade' => $grade,
'types' => TypeLesson::cases(),
]);
}
/**
* Update the specified resource in storage.
*/
public function update(LessonPostRequest $request, Grade $grade, Lesson $lesson): RedirectResponse
{
return redirect()->route(
'grades.lessons.show',[
$grade,
$this->service->update($lesson, $request->validated())
]
);
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Grade $grade, Lesson $lesson): RedirectResponse
{
$this->service->delete($lesson);
return redirect()->route('grades.lessons.index', $grade);
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Http\Controllers;
use App\Enums\ScoreEnum;
use App\Models\Lesson;
use App\Services\ServiceInterface;
use Illuminate\Http\Request;
class ScoreController extends Controller
{
public function __construct(
protected ServiceInterface $service,
){
}
public function show(Lesson $lesson)
{
return view('scores.show', [
'students' => $this->service->getAll($lesson),
'lesson' => $lesson,
'scores' => ScoreEnum::cases(),
]);
}
public function update(Request $request, Lesson $lesson)
{
$this->service->update($lesson, $request->toArray());
return redirect()->route('grades.lessons.show', [$lesson->grade, $lesson]);
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Http\Requests;
use App\Enums\TypeLesson;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class LessonPostRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'type' => ['required', Rule::in(TypeLesson::cases())],
'lesson_date' => 'required|date',
'description' => 'nullable|string|max:250',
'grade_id' => 'required|exists:grades,id',
'teacher_id' => 'required|exists:teachers,id',
'subject_id' => 'required|exists:subjects,id',
];
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ScorePostRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return false;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
//
];
}
}

View File

@ -31,6 +31,11 @@ class Grade extends Model
return $this->belongsToMany(Teacher::class);
}
public function lessons(): HasMany
{
return $this->hasMany(Lesson::class);
}
public function scopeFilter(Builder $query): void
{
$name = request('name');

54
app/Models/Lesson.php Normal file
View File

@ -0,0 +1,54 @@
<?php
namespace App\Models;
use Illuminate\Contracts\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Lesson extends Model
{
use HasFactory;
protected $fillable = [
'name',
'type',
'description',
'lesson_date',
'grade_id',
'teacher_id',
'subject_id',
];
public function grade(): BelongsTo
{
return $this->belongsTo(Grade::class);
}
public function teacher(): BelongsTo
{
return $this->belongsTo(Teacher::class);
}
public function subject(): BelongsTo
{
return $this->belongsTo(Subject::class);
}
public function students(): BelongsToMany
{
return $this->belongsToMany(Student::class)->withPivot('score');
}
public function scopeFilter(Builder $query): void
{
$subject_id = request('subject_id');
$query->when($subject_id, function (Builder $query, $subject_id) {
$query->where('subject_id', $subject_id);
});
}
}

View File

@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
class Student extends Model
@ -31,6 +32,16 @@ class Student extends Model
return $this->morphOne(User::class, 'userable');
}
public function subjects(): BelongsToMany
{
return $this->belongsToMany(Subject::class);
}
public function lessons(): BelongsToMany
{
return $this->belongsToMany(Lesson::class);
}
public function scopeFilter(Builder $query): void
{
$name = request('name');

View File

@ -6,6 +6,7 @@ use Illuminate\Contracts\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
class Subject extends Model
@ -26,6 +27,16 @@ class Subject extends Model
return $this->belongsToMany(Teacher::class);
}
public function students(): BelongsToMany
{
return $this->belongsToMany(Student::class)->using(Score::class);
}
public function lessons(): HasMany
{
return $this->hasMany(Lesson::class);
}
public function scopeFilter(Builder $query): void
{
$name = request('name');

View File

@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
class Teacher extends Model
@ -35,6 +36,11 @@ class Teacher extends Model
return $this->morphOne(User::class, 'userable');
}
public function lessons(): HasMany
{
return $this->hasMany(Lesson::class);
}
public function scopeFilter(Builder $query): void
{
$name = request('name');

View File

@ -5,6 +5,8 @@ namespace App\Providers;
use App\Http\Controllers\GradeController;
use App\Http\Controllers\GradeSubjectController;
use App\Http\Controllers\GradeTeacherController;
use App\Http\Controllers\LessonController;
use App\Http\Controllers\ScoreController;
use App\Http\Controllers\StudentController;
use App\Http\Controllers\SubjectController;
use App\Http\Controllers\SubjectTeacherController;
@ -12,6 +14,8 @@ use App\Http\Controllers\TeacherController;
use App\Services\GradeService;
use App\Services\GradeSubjectService;
use App\Services\GradeTeacherService;
use App\Services\LessonService;
use App\Services\ScoreService;
use App\Services\ServiceInterface;
use App\Services\StudentService;
use App\Services\SubjectService;
@ -67,6 +71,19 @@ class ModelServiceProvider extends ServiceProvider
->give(function () {
return new GradeTeacherService();
});
$this->app->when(LessonController::class)
->needs(ServiceInterface::class)
->give(function () {
return new LessonService();
});
$this->app->when(ScoreController::class)
->needs(ServiceInterface::class)
->give(function () {
return new ScoreService();
});
}
/**

View File

@ -0,0 +1,45 @@
<?php
namespace App\Services;
use App\Enums\ScoreEnum;
use App\Models\Grade;
use App\Models\Lesson;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
class LessonService implements ServiceInterface
{
public function getAll(?Grade $grade = null): Collection
{
return $grade->lessons()->filter()->get();
}
public function getGrades(): Collection
{
return Grade::all();
}
public function create(array $data): Lesson
{
$lesson = Lesson::create($data);
$lesson
->students()
->syncWithPivotValues($lesson->grade->students->pluck('id')->all(), ['score' => ScoreEnum::WithoutScore]);
return $lesson;
}
public function update(Model $model, array $data): Lesson
{
$model->update($data);
return $model;
}
public function delete($model): void
{
$model->delete();
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Services;
use App\Models\Lesson;
use Illuminate\Database\Eloquent\Model;
class ScoreService implements ServiceInterface
{
public function getAll(?Lesson $lesson = null)
{
return $lesson->students;
}
public function update(Model $model, array $data)
{
$model->students->each(function ($item, $key) use ($data, $model) {
if ($data['score' . $item->id]) {
$model->students()->syncWithoutDetaching([$item->id => ['score' => $data['score' . $item->id]]]);
}
});
return $model;
}
public function delete(Model $model)
{
// TODO: Implement delete() method.
}
public function create(array $data)
{
// TODO: Implement create() method.
}
}

View File

@ -8,9 +8,9 @@ interface ServiceInterface
{
public function getAll();
public function create(array $data): Model;
public function create(array $data);
public function update(Model $model, array $data): Model;
public function update(Model $model, array $data);
public function delete(Model $model);
}

View File

@ -2,6 +2,7 @@
namespace App\Services;
use App\Enums\ScoreEnum;
use App\Models\Student;
use App\Models\User;
use Illuminate\Pagination\LengthAwarePaginator;
@ -27,6 +28,9 @@ class StudentService implements ServiceInterface
'grade_id' => $data['grade_id'],
]);
$student->user()->save($user);
$student
->lessons()
->syncWithPivotValues($student->grade->lessons->pluck('id')->all(), ['score' => ScoreEnum::WithoutScore]);
return $student;
}

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('lessons', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('description')->nullable();
$table->string('type');
$table->date('lesson_date');
$table->foreignId('grade_id')->constrained('grades')->onDelete('cascade');
$table->foreignId('subject_id')->constrained('subjects')->onDelete('cascade');
$table->foreignId('teacher_id')->constrained('teachers')->onDelete('cascade');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('lessons');
}
};

View File

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('lesson_student', function (Blueprint $table) {
$table->foreignId('lesson_id')->constrained('lessons')->onDelete('cascade');
$table->foreignId('student_id')->constrained('students')->onDelete('cascade');
$table->string('score');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('lesson_student');
}
};

View File

@ -0,0 +1,5 @@
@extends('layouts.app')
@section('content')
@include('grade-lesson.form', ['route' => route('grades.lessons.store', $grade), 'method' => 'POST'])
@endsection

View File

@ -0,0 +1,5 @@
@extends('layouts.app')
@section('content')
@include('grade-lesson.form', ['route' => route('grades.lessons.update', [$grade, $lesson]), 'method' => 'PUT'])
@endsection

View File

@ -0,0 +1,51 @@
<div class="container col-md-5">
<div class="row justify-content-center">
<div class="card mt-4">
<div class="card-body">
<form action="{{ $route }}" method="POST">
@csrf
@method($method)
<div class="mb-3">
<label for="name" class="form-label">Тема:</label>
<input type="text" id="name" name="name" class="form-control" value="{{ isset($lesson) ? $lesson->name : old('name') }}" required>
</div>
<div class="mb-3">
<label for="type" class="form-label">Тип работы:</label>
<select id="type" name="type" class="form-select" required>
<option value="">Выберите тип деятельности</option>
@foreach($types as $type)
<option value="{{ $type }}" {{ isset($lesson) && $lesson->type == $type ? 'selected' : old('type') }}>
{{ $type }}
</option>
@endforeach
</select>
</div>
<div class="mb-3">
<label for="subject_id" class="form-label">Предмет:</label>
<select id="subject_id" name="subject_id" class="form-select" required>
<option value="">Выберите предмет</option>
@foreach($grade->subjects as $subject)
<option value="{{ $subject->id }}" {{ isset($lesson) && $lesson->subject_id == $subject->id ? 'selected' : old('subject_id') }}>
{{ $subject->name }}
</option>
@endforeach
</select>
</div>
<div class="mb-3">
<label for="description" class="form-label">Описание(макс. 250 символов):</label>
<textarea class="form-control" id="description" name="description" rows="5" >{{ isset($lesson) ? $lesson->description : old('description') }}</textarea>
</div>
<div class="mb-3">
<label for="lesson_date" class="form-label">Дата рождения:</label>
<input type="date" id="lesson_date" name="lesson_date" class="form-control" value="{{ isset($lesson) ? $lesson->lesson_date : old('lesson_date') }}" required>
</div>
<input type="hidden" name="grade_id" value="{{ $grade->id }}">
<input type="hidden" name="teacher_id" value="{{ 1 }}"> {{-- заглушка --}}
<div>
<button type="submit" class="btn btn-success">Подтвердить</button>
</div>
</form>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,57 @@
`@extends('layouts.app')
@section('content')
<div class="container col-md-5">
<div class="row justify-content-center">
<div class="card mt-4">
<div class="card-header">{{__('Создание занятия')}}</div>
<div class="card-body">
<a href="{{ route('grades.lessons.create', $grade) }}" class="btn btn-success">Добавить</a>
</div>
</div>
<div class="card mt-4">
<div class="card-header">{{__('Фильтрация')}}</div>
<div class="card-body">
<form action="{{ route('grades.lessons.index', $grade) }}" method="GET">
<div class="mb-3">
<label for="subject_id" class="form-label">Предмет:</label>
<select id="subject_id" name="subject_id" class="form-select" required>
<option value="">Выберите предмет</option>
@foreach($subjects as $subject)
<option value="{{ $subject->id }}"> {{ $subject->id == request('subject_id') ? 'selected' : ''}}
{{ $subject->name }}
</option>
@endforeach
</select>
</div>
<div>
<button type="submit" class="btn btn-primary">Фильтр</button>
</div>
</form>
</div>
</div>
@if(count($lessons))
@foreach($lessons as $lesson)
<a href="{{ route('grades.lessons.show', [$grade, $lesson]) }}" class="text-decoration-none">
<div class="card mt-4">
<div class="card-header"><strong>{{$lesson->subject->name}}</strong></div>
<div class="card-header">{{$lesson->type}}</div>
<div class="card-body">
{{ $lesson->name }}
</div>
<div class="card-footer">
<form action="{{ route('grades.lessons.destroy', [$grade, $lesson]) }}" method="POST" style="display: inline-block;">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger">Удалить</button>
</form>
</div>
</div>
</a>
@endforeach
@endif
</div>
</div>
@endsection
`

View File

@ -0,0 +1,31 @@
@extends('layouts.app')
@section('content')
<div class="container col-md-6">
<div class="row justify-content-center">
<div class="card">
<div class="card-header">{{ $lesson->type }}</div>
<div class="card-body">
<div class="mb-3">
<h5><strong>Тема: </strong>{{ $lesson->name }}</h5>
</div>
<div class="mb-3">
<h5><strong>Предмет: </strong>{{ $lesson->subject->name }}</h5>
</div>
<div class="mb-3">
<h5><strong>Дата: </strong>{{ $lesson->lesson_date }}</h5>
</div>
<div class="mb-3">
<h5><strong>Описание: </strong>{{ $lesson->description }}</h5>
</div>
</div>
<div class="card-footer">
<a href="{{ route('lessons.scores.show', $lesson) }}" class="btn btn-success">Оценки</a>
<a href="{{ route('grades.lessons.edit', [$grade, $lesson]) }}" class="btn btn-primary">Редактировать</a>
</div>
</div>
</div>
</div>
@endsection

View File

@ -36,16 +36,16 @@
class="btn btn-block col-8">{{ $grade->name }}</a>
</div>
</td>
<td>
<div>
<a href="{{ route('grades.lessons.index', $grade) }}" class="btn btn-primary">Занятия</a>
</div>
</td>
<td>
<div>
<a href="{{ route('grades.edit', $grade) }}" class="btn btn-warning">Редактировать</a>
</div>
</td>
{{-- <td>--}}
{{-- <div>--}}
{{-- <a href="{{ route('journals.index', $grade) }}" class="btn btn-primary">Журнал</a>--}}
{{-- </div>--}}
{{-- </td>--}}
<td>
<form action="{{ route('grades.destroy', $grade) }}" method="POST"
style="display: inline-block;">

View File

@ -2,7 +2,7 @@
<header class="d-flex justify-content-center py-3">
<ul class="nav nav-pills">
<li class="nav-item"><a href="{{ route('grades.index') }}" class="nav-link @if(request()->is('grades*')) active @endif" aria-current="page">Классы</a></li>
<li class="nav-item"><a href="{{ route('subjects.index') }}" class="nav-link @if(request()->is('subjects*')) active @endif">Предметы</a></li>
<li class="nav-item"><a href="{{ route('subjects.index') }}" class="nav-link @if(request()->is('subjects*')) active @endif">Предметы</a>
<li class="nav-item"><a href="{{ route('students.index') }}" class="nav-link @if(request()->is('students*')) active @endif">Ученики</a></li>
<li class="nav-item"><a href="{{ route('teachers.index') }}" class="nav-link @if(request()->is('teachers*')) active @endif">Учителя</a></li>
</ul>

View File

@ -0,0 +1,52 @@
@extends('layouts.app')
@section('content')
<div class="container col-md-4">
<div class="row justify-content-center">
<div class="card mt-4">
<div class="card-header">
<p>{{ $lesson->name }}</p>
</div>
<div class="card-body">
<form action="{{ route('lessons.scores.update', [$lesson]) }}" method="POST">
@csrf
@method('PUT')
@if(count($students))
<table class="table table-striped">
<thead>
<th>ФИО</th>
<th>Оценка</th>
</thead>
<tbody>
@foreach ($students as $student)
<tr>
<td class="table-text">
<div>
<p>{{ $student->fio }}</p>
</div>
</td>
<td>
<div>
<select id="score" name="score{{ $student->id }}" class="form-select form-select-sm">
@foreach($scores as $score)
<option value="{{ $score }}" {{ isset($student->pivot->score) && $student->pivot->score == $score->value ? 'selected' : '' }}>
{{ $score }}
</option>
@endforeach
</select>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
@else
<p>Ученики остутствуют</p>
@endif
<button type="submit" class="btn btn-success">Подтвердить</button>
</form>
</div>
</div>
</div>
</div>
@endsection

View File

@ -31,7 +31,7 @@
<tr>
<td class="table-text">
<div>
<a href="{{ route('students.show', $student) }}" class="btn btn-block col-8">{{ $student->last_name }} {{ $student->name }} {{ $student->middle_name }}</a>
<a href="{{ route('students.show', $student) }}" class="btn btn-block col-8">{{ $student->fio }}</a>
</div>
</td>
<td>

View File

@ -3,6 +3,8 @@
use App\Http\Controllers\GradeController;
use App\Http\Controllers\GradeSubjectController;
use App\Http\Controllers\GradeTeacherController;
use App\Http\Controllers\LessonController;
use App\Http\Controllers\ScoreController;
use App\Http\Controllers\StudentController;
use App\Http\Controllers\SubjectController;
use App\Http\Controllers\SubjectTeacherController;
@ -18,8 +20,11 @@ Route::resources([
'subjects' => SubjectController::class,
'students' => StudentController::class,
'teachers' => TeacherController::class,
'grades.lessons' => LessonController::class,
]);
Route::resource('teachers.subjects', SubjectTeacherController::class)->except('index');
Route::resource('teachers.subjects.grades', GradeTeacherController::class)->except('index', 'show');
Route::resource('grades.subjects', GradeSubjectController::class)->except('index', 'show');
Route::get('lessons/{lesson}/scores', [ScoreController::class, 'show'])->name('lessons.scores.show');
Route::put('lessons/{lesson}/scores', [ScoreController::class, 'update'])->name('lessons.scores.update');