Initial commit
This commit is contained in:
commit
c909d539bc
7178 files changed
+756624
No files matched your search
@@ -0,0 +1,276 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Student;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Slot;
|
||||
use Carbon\Carbon;
|
||||
use App\Services\AvailabilityService;
|
||||
|
||||
class BookingController extends Controller
|
||||
{
|
||||
public function create(Request $request)
|
||||
{
|
||||
// Expect: /student/bookings/create?date=YYYY-MM-DD&slot_id=NN
|
||||
$data = $request->validate([
|
||||
'date' => ['required','date_format:Y-m-d'],
|
||||
'slot_id' => ['required','exists:slots,id'],
|
||||
]);
|
||||
|
||||
$slot = Slot::findOrFail($data['slot_id']);
|
||||
|
||||
// Prettify labels so Blade doesn’t need Carbon
|
||||
$when = Carbon::createFromFormat('Y-m-d H:i:s', "{$data['date']} {$slot->time}", 'Europe/London');
|
||||
$prettyDate = $when->format('D d M Y');
|
||||
$prettyTime = $when->format('H:i');
|
||||
$weekdayName = $when->format('l');
|
||||
|
||||
return view('student.bookings.create', [
|
||||
'slot' => $slot,
|
||||
'date' => $data['date'],
|
||||
'prettyDate' => $prettyDate,
|
||||
'prettyTime' => $prettyTime,
|
||||
'weekdayName' => $weekdayName,
|
||||
]);
|
||||
}
|
||||
|
||||
public function createSeries(\Illuminate\Http\Request $request)
|
||||
{
|
||||
// Ensure these use's exist at the top of the file:
|
||||
// use App\Models\Slot;
|
||||
// use Carbon\Carbon;
|
||||
|
||||
// Expect: /student/bookings/series/create?date=YYYY-MM-DD&slot_id=NN&weeks=6
|
||||
$data = $request->validate([
|
||||
'date' => ['required','date_format:Y-m-d'],
|
||||
'slot_id' => ['required','exists:slots,id'],
|
||||
'weeks' => ['nullable','integer','min:1','max:52'],
|
||||
]);
|
||||
|
||||
$slot = \App\Models\Slot::findOrFail($data['slot_id']);
|
||||
|
||||
// Prettify labels (keep Blade Carbon-free)
|
||||
$when = \Carbon\Carbon::createFromFormat('Y-m-d H:i:s', "{$data['date']} {$slot->time}", 'Europe/London');
|
||||
$prettyDate = $when->format('D d M Y');
|
||||
$prettyTime = $when->format('H:i');
|
||||
$weekdayName = $when->format('l');
|
||||
|
||||
$weeks = $data['weeks'] ?? 6;
|
||||
|
||||
return view('student.bookings.series', [
|
||||
'slot' => $slot,
|
||||
'date' => $data['date'],
|
||||
'prettyDate' => $prettyDate,
|
||||
'prettyTime' => $prettyTime,
|
||||
'weekdayName' => $weekdayName,
|
||||
'weeks' => $weeks,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(\Illuminate\Http\Request $request)
|
||||
{
|
||||
// Ensure these use's exist at the top of the file:
|
||||
// use App\Models\Slot;
|
||||
// use App\Models\Booking;
|
||||
// use Carbon\Carbon;
|
||||
// use Illuminate\Validation\ValidationException;
|
||||
// use Illuminate\Database\QueryException;
|
||||
|
||||
$data = $request->validate([
|
||||
'slot_id' => ['required','exists:slots,id'],
|
||||
'date' => ['required','date_format:Y-m-d'],
|
||||
'message' => ['nullable','string'],
|
||||
]);
|
||||
|
||||
$slot = \App\Models\Slot::findOrFail($data['slot_id']);
|
||||
|
||||
// Enforce > 2 hours’ notice (exactly 120 mins = NOT allowed)
|
||||
$now = \Carbon\Carbon::now('Europe/London');
|
||||
$sessionStart = \Carbon\Carbon::createFromFormat('Y-m-d H:i:s', "{$data['date']} {$slot->time}", 'Europe/London');
|
||||
$minutesUntil = $now->diffInMinutes($sessionStart, false);
|
||||
if ($minutesUntil <= 120) {
|
||||
throw \Illuminate\Validation\ValidationException::withMessages([
|
||||
'date' => 'Bookings require more than 2 hours’ notice.',
|
||||
]);
|
||||
}
|
||||
|
||||
// Pull student name from the logged-in user
|
||||
$user = $request->user();
|
||||
$studentName = $user?->name ?? 'Student';
|
||||
|
||||
try {
|
||||
\App\Models\Booking::create([
|
||||
'slot_id' => $slot->id,
|
||||
'date' => $data['date'],
|
||||
'student_name' => $studentName,
|
||||
'booked_from_page' => 'student:booking',
|
||||
'message' => $data['message'] ?? null,
|
||||
'status' => 'booked',
|
||||
]);
|
||||
} catch (\Illuminate\Database\QueryException $e) {
|
||||
// Unique (slot_id, date) hit — slot already taken
|
||||
if (isset($e->errorInfo[1]) && (int)$e->errorInfo[1] === 1062) {
|
||||
throw \Illuminate\Validation\ValidationException::withMessages([
|
||||
'slot_id' => 'Sorry, that slot was just taken. Please choose another.',
|
||||
]);
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('student.calendar')
|
||||
->with('success', 'Your booking is confirmed.');
|
||||
}
|
||||
|
||||
public function storeSeries(\Illuminate\Http\Request $request)
|
||||
{
|
||||
// Validates: slot, start date, and weeks count
|
||||
$data = $request->validate([
|
||||
'slot_id' => ['required','exists:slots,id'],
|
||||
'date' => ['required','date_format:Y-m-d'], // start date
|
||||
'weeks' => ['required','integer','min:1','max:52'],
|
||||
]);
|
||||
|
||||
$slot = \App\Models\Slot::findOrFail($data['slot_id']);
|
||||
$now = \Carbon\Carbon::now('Europe/London');
|
||||
$startDate = \Carbon\Carbon::createFromFormat('Y-m-d', $data['date'], 'Europe/London');
|
||||
$user = $request->user();
|
||||
$student = $user?->name ?? 'Student';
|
||||
|
||||
$created = 0;
|
||||
$skipped = 0;
|
||||
|
||||
for ($i = 0; $i < (int)$data['weeks']; $i++) {
|
||||
$date = $startDate->copy()->addWeeks($i)->toDateString();
|
||||
$sessionStart = \Carbon\Carbon::createFromFormat('Y-m-d H:i:s', "{$date} {$slot->time}", 'Europe/London');
|
||||
|
||||
// Enforce > 2 hours’ notice (exactly 120 mins = NOT allowed)
|
||||
$minutesUntil = $now->diffInMinutes($sessionStart, false);
|
||||
if ($minutesUntil <= 120) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
\App\Models\Booking::create([
|
||||
'slot_id' => $slot->id,
|
||||
'date' => $date,
|
||||
'student_name' => $student,
|
||||
'booked_from_page' => 'student:block-series',
|
||||
'message' => null,
|
||||
'status' => 'booked',
|
||||
]);
|
||||
$created++;
|
||||
} catch (\Illuminate\Database\QueryException $e) {
|
||||
// Skip duplicates (unique (slot_id, date))
|
||||
if (isset($e->errorInfo[1]) && (int)$e->errorInfo[1] === 1062) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('student.calendar')
|
||||
->with('success', "Series created: {$created} added, {$skipped} skipped.");
|
||||
}
|
||||
|
||||
public function cancelConfirm(\App\Models\Booking $booking)
|
||||
{
|
||||
// Ensure these use's exist at the top:
|
||||
// use App\Models\Booking;
|
||||
// use Carbon\Carbon;
|
||||
|
||||
$user = auth()->user();
|
||||
$studentName = $user?->name ?? '';
|
||||
|
||||
// Authorize: must be this student's booking and still "booked"
|
||||
if ($booking->student_name !== $studentName || $booking->status !== 'booked') {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
// Need the slot's time to compute the session start
|
||||
$booking->load('slot');
|
||||
|
||||
$now = \Carbon\Carbon::now('Europe/London');
|
||||
$sessionStart = \Carbon\Carbon::createFromFormat(
|
||||
'Y-m-d H:i:s',
|
||||
$booking->date.' '.$booking->slot->time,
|
||||
'Europe/London'
|
||||
);
|
||||
|
||||
$minutesUntil = $now->diffInMinutes($sessionStart, false);
|
||||
|
||||
// If already started or in the past, disallow cancellation
|
||||
if ($minutesUntil <= 0) {
|
||||
return redirect()
|
||||
->route('student.bookings.index')
|
||||
->withErrors(['booking' => 'This session has started or is in the past and can’t be cancelled.']);
|
||||
}
|
||||
|
||||
// Refund policy band (for display only; payments added later)
|
||||
// ≥ 24h => 100%; < 24h => 50%
|
||||
$refundPercent = ($minutesUntil >= 24 * 60) ? 100 : 50;
|
||||
|
||||
// Prettified labels so Blade stays Carbon-free
|
||||
$prettyDate = $sessionStart->format('D d M Y');
|
||||
$prettyTime = $sessionStart->format('H:i');
|
||||
$weekdayName = $sessionStart->format('l');
|
||||
|
||||
return view('student.bookings.cancel', [
|
||||
'booking' => $booking,
|
||||
'prettyDate' => $prettyDate,
|
||||
'prettyTime' => $prettyTime,
|
||||
'weekdayName' => $weekdayName,
|
||||
'refundPercent' => $refundPercent,
|
||||
'minutesUntil' => $minutesUntil,
|
||||
]);
|
||||
}
|
||||
|
||||
public function cancelStore(\App\Models\Booking $booking)
|
||||
{
|
||||
// Ensure these use's exist at the top:
|
||||
// use App\Models\Booking;
|
||||
// use Carbon\Carbon;
|
||||
|
||||
$user = auth()->user();
|
||||
$studentName = $user?->name ?? '';
|
||||
|
||||
// Authorize: must be this student's booking and still "booked"
|
||||
if ($booking->student_name !== $studentName || $booking->status !== 'booked') {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
// Need slot time to compute the session start
|
||||
$booking->load('slot');
|
||||
|
||||
$now = \Carbon\Carbon::now('Europe/London');
|
||||
$sessionStart = \Carbon\Carbon::createFromFormat(
|
||||
'Y-m-d H:i:s',
|
||||
$booking->date.' '.$booking->slot->time,
|
||||
'Europe/London'
|
||||
);
|
||||
|
||||
$minutesUntil = $now->diffInMinutes($sessionStart, false);
|
||||
|
||||
// If already started or in the past, disallow cancellation
|
||||
if ($minutesUntil <= 0) {
|
||||
return redirect()
|
||||
->route('student.bookings.index')
|
||||
->withErrors(['booking' => 'This session has started or is in the past and can’t be cancelled.']);
|
||||
}
|
||||
|
||||
// Refund band (for info only; payments to be integrated later)
|
||||
$refundPercent = ($minutesUntil >= 24 * 60) ? 100 : 50;
|
||||
|
||||
// For now, delete the booking to free the slot.
|
||||
// (Later we can switch to a "cancelled" status or soft deletes if you want a history.)
|
||||
$booking->delete();
|
||||
|
||||
return redirect()
|
||||
->route('student.bookings.index')
|
||||
->with('success', "Booking cancelled. Refund: {$refundPercent}%.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Student;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BookingsListController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
// Ensure these use's exist at the top of the file:
|
||||
// use App\Models\Booking;
|
||||
// use Carbon\Carbon;
|
||||
|
||||
$user = auth()->user();
|
||||
$name = $user?->name ?? '';
|
||||
$now = \Carbon\Carbon::now('Europe/London');
|
||||
$today = $now->toDateString();
|
||||
|
||||
// Upcoming “booked” sessions for this student (names-based for now)
|
||||
$raw = \App\Models\Booking::with('slot')
|
||||
->where('student_name', $name)
|
||||
->where('status', 'booked')
|
||||
->whereDate('date', '>=', $today)
|
||||
->orderBy('date')
|
||||
->get();
|
||||
|
||||
// Pre-format labels so Blade stays Carbon-free
|
||||
$bookings = $raw->map(function ($b) use ($now) {
|
||||
$when = \Carbon\Carbon::createFromFormat('Y-m-d H:i:s', $b->date.' '.$b->slot->time, $now->timezone);
|
||||
return [
|
||||
'id' => $b->id,
|
||||
'date' => $b->date,
|
||||
'prettyDate' => $when->format('D d M Y'),
|
||||
'prettyTime' => $when->format('H:i'),
|
||||
'weekday' => $when->format('l'),
|
||||
'status' => $b->status,
|
||||
'slot_id' => $b->slot_id,
|
||||
];
|
||||
});
|
||||
|
||||
return view('student.bookings.index', [
|
||||
'bookings' => $bookings,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Student;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Slot;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class CalendarController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$now = Carbon::now('Europe/London');
|
||||
|
||||
// Default: this Mon–Sun, but flip to next week after Fri 15:00
|
||||
$start = $now->copy()->startOfWeek(Carbon::MONDAY);
|
||||
$fridayCutoff = $start->copy()->addDays(4)->setTime(15, 0);
|
||||
if ($now->greaterThanOrEqualTo($fridayCutoff)) {
|
||||
$start->addWeek();
|
||||
}
|
||||
|
||||
// Week navigation
|
||||
$weekOffset = (int) $request->query('week', 0);
|
||||
if ($weekOffset !== 0) {
|
||||
$start->addWeeks($weekOffset);
|
||||
}
|
||||
$end = $start->copy()->addDays(6);
|
||||
|
||||
// Load slots + bookings for this visible week
|
||||
$slots = Slot::with(['bookings' => function ($q) use ($start, $end) {
|
||||
$q->whereBetween('date', [$start->toDateString(), $end->toDateString()]);
|
||||
}])->get();
|
||||
|
||||
// Reuse the same view for now (we'll hide admin-only bits next)
|
||||
return view('calendar.index', [
|
||||
'start' => $start,
|
||||
'end' => $end,
|
||||
'weekOffset' => $weekOffset,
|
||||
'slots' => $slots,
|
||||
'now' => $now,
|
||||
'viewer' => 'student',
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Student;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CourseController extends Controller
|
||||
{
|
||||
public function show(Course $course)
|
||||
{
|
||||
// Load relationships so Blade can access subject, level, modules etc.
|
||||
$course->load('subject', 'level', 'modules.lessonSections.chapters.lessons');
|
||||
|
||||
return view('student.courses.show', compact('course'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Student;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Course;
|
||||
use App\Models\Lesson;
|
||||
use App\Models\LessonProgress;
|
||||
use App\Models\User;
|
||||
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
// 1) Enrolled courses (with subject & level)
|
||||
$enrolled = $user->courses()
|
||||
->with(['subject', 'level'])
|
||||
->get();
|
||||
|
||||
// 2) Courses not yet enrolled
|
||||
$availableCourses = Course::with(['subject', 'level'])
|
||||
->whereNotIn('id', $enrolled->pluck('id'))
|
||||
->get();
|
||||
|
||||
// 3) Find which courses this user has started (based on LessonProgress)
|
||||
$progressEntries = LessonProgress::where('user_id', $user->id)
|
||||
->with('lesson.chapter.lessonSection.module.course')
|
||||
->orderByDesc('last_viewed_at')
|
||||
->get();
|
||||
|
||||
$startedCourseIds = $progressEntries
|
||||
->pluck('lesson.chapter.lessonSection.module.course.id')
|
||||
->unique();
|
||||
|
||||
// Split enrolled courses into inProgress and notStarted
|
||||
[$inProgressCourses, $notStartedCourses] = $enrolled->partition(
|
||||
fn($course) => $startedCourseIds->contains($course->id)
|
||||
);
|
||||
|
||||
// 4) Build map of the *latest viewed lesson per course*
|
||||
$lastLessonsByCourse = collect();
|
||||
|
||||
foreach ($progressEntries as $entry) {
|
||||
$course = $entry->lesson->chapter->lessonSection->module->course;
|
||||
$courseId = $course->id;
|
||||
|
||||
// Store the first (i.e. most recent, since entries are sorted DESC) occurrence
|
||||
if (!$lastLessonsByCourse->has($courseId)) {
|
||||
$lastLessonsByCourse->put($courseId, $entry->lesson);
|
||||
}
|
||||
}
|
||||
|
||||
return view('student.dashboard', [
|
||||
'inProgressCourses' => $inProgressCourses,
|
||||
'notStartedCourses' => $notStartedCourses,
|
||||
'availableCourses' => $availableCourses,
|
||||
'lastLessonsByCourse' => $lastLessonsByCourse,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Student;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Course;
|
||||
use App\Models\Lesson;
|
||||
use App\Models\LessonProgress;
|
||||
|
||||
class LessonController extends Controller
|
||||
{
|
||||
public function show(Course $course, Lesson $lesson)
|
||||
{
|
||||
if (
|
||||
$lesson->chapter &&
|
||||
$lesson->chapter->lessonSection &&
|
||||
$lesson->chapter->lessonSection->module &&
|
||||
$lesson->chapter->lessonSection->module->course &&
|
||||
$lesson->chapter->lessonSection->module->course->id !== $course->id
|
||||
) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$lesson->load([
|
||||
'chapter.lessonSection.module.course.subject',
|
||||
'chapter.lessonSection.module.course.level',
|
||||
'questions' => fn($q) => $q->where('published', 1)->with('student'),
|
||||
]);
|
||||
|
||||
LessonProgress::updateOrCreate(
|
||||
['user_id' => Auth::id(), 'lesson_id' => $lesson->id],
|
||||
['last_viewed_at' => now()]
|
||||
);
|
||||
|
||||
$course = $lesson->chapter->lessonSection->module->course;
|
||||
|
||||
// Collect all chapters across all lesson sections for this course
|
||||
$chapters = $course->lessonSections()
|
||||
->with(['chapters.lessons'])
|
||||
->get()
|
||||
->pluck('chapters')
|
||||
->flatten();
|
||||
|
||||
return view('student.lessons.show', compact('lesson', 'chapters'));
|
||||
}
|
||||
|
||||
|
||||
// Pretty route: /student/{course}/{lesson}
|
||||
public function showByCourse(Course $course, Lesson $lesson)
|
||||
{
|
||||
|
||||
\Log::info('Entered showByCourse route');
|
||||
dd('Reached controller');
|
||||
dd($course, $lesson);
|
||||
// Ensure the lesson actually belongs to the course
|
||||
$belongs = $lesson->chapter
|
||||
&& $lesson->chapter->lessonSection
|
||||
&& $lesson->chapter->lessonSection->module
|
||||
&& $lesson->chapter->lessonSection->module->course
|
||||
&& $lesson->chapter->lessonSection->module->course->id === $course->id;
|
||||
|
||||
if (!$belongs) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
return $this->show($lesson); // reuse the existing rendering logic
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Student;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use App\Models\Lesson;
|
||||
use App\Models\Course;
|
||||
use App\Models\LessonQuestion;
|
||||
|
||||
class LessonQuestionController extends Controller
|
||||
{
|
||||
/**
|
||||
* Store a new question from a student.
|
||||
*/
|
||||
public function store(Request $request, Course $course, Lesson $lesson)
|
||||
{
|
||||
$request->validate([
|
||||
'question' => 'required|string|max:2000',
|
||||
]);
|
||||
|
||||
LessonQuestion::create([
|
||||
'lesson_id' => $lesson->id,
|
||||
'user_id' => Auth::id(),
|
||||
'question' => $request->question,
|
||||
'published' => 0,
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Your question has been submitted.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional — allow teachers (or admins) to post an answer.
|
||||
*/
|
||||
public function answer(Request $request, LessonQuestion $question)
|
||||
{
|
||||
$request->validate([
|
||||
'answer' => 'required|string|max:5000',
|
||||
]);
|
||||
|
||||
$question->update([
|
||||
'answer' => $request->answer,
|
||||
'answered_by' => Auth::id(),
|
||||
'answered_at' => now(),
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Answer posted successfully.');
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user