Initial commit
This commit is contained in:
commit
c909d539bc
7178 files changed
+756624
No files matched your search
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\BillingAccount;
|
||||
|
||||
class BillingController extends Controller
|
||||
{
|
||||
public function checkEmail(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'email' => 'required|email',
|
||||
]);
|
||||
|
||||
$billing = BillingAccount::where('invoice_email', $request->email)->first();
|
||||
|
||||
if ($billing) {
|
||||
return response()->json([
|
||||
'exists' => true,
|
||||
'billing_account_id' => $billing->id,
|
||||
'billing_name' => $billing->name,
|
||||
'primary_parent_user_id' => $billing->primary_parent_user_id,
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'exists' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Slot;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class BookingController extends Controller
|
||||
{
|
||||
public function create(Request $request)
|
||||
{
|
||||
// Validate query params: /admin/bookings/create?date=YYYY-MM-DD&slot_id=123
|
||||
$data = $request->validate([
|
||||
'date' => ['required','date_format:Y-m-d'],
|
||||
'slot_id' => ['required','exists:slots,id'],
|
||||
]);
|
||||
|
||||
$slot = Slot::findOrFail($data['slot_id']);
|
||||
|
||||
// Build nice labels (Europe/London) 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('bookings.create', [
|
||||
'slot' => $slot,
|
||||
'date' => $data['date'],
|
||||
'prettyDate' => $prettyDate,
|
||||
'prettyTime' => $prettyTime,
|
||||
'weekdayName' => $weekdayName,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(\Illuminate\Http\Request $request)
|
||||
{
|
||||
// Needed at top of file (ensure these use's exist):
|
||||
// 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'],
|
||||
'student_name' => ['required','string','max:255'],
|
||||
'booked_from_page' => ['nullable','string','max:255'],
|
||||
'message' => ['nullable','string'],
|
||||
]);
|
||||
|
||||
// Enforce > 2 hours' notice (exactly 120 mins = NOT allowed)
|
||||
$slot = \App\Models\Slot::findOrFail($data['slot_id']);
|
||||
$sessionStart = \Carbon\Carbon::createFromFormat('Y-m-d H:i:s', "{$data['date']} {$slot->time}", 'Europe/London');
|
||||
$now = \Carbon\Carbon::now('Europe/London');
|
||||
$minutesUntil = $now->diffInMinutes($sessionStart, false);
|
||||
|
||||
if ($minutesUntil <= 120) {
|
||||
throw \Illuminate\Validation\ValidationException::withMessages([
|
||||
'date' => 'Bookings require more than 2 hours’ notice.',
|
||||
]);
|
||||
}
|
||||
|
||||
// Persist booking; default status for admin-created bookings = 'booked'
|
||||
$data['status'] = 'booked';
|
||||
|
||||
try {
|
||||
\App\Models\Booking::create($data);
|
||||
} catch (\Illuminate\Database\QueryException $e) {
|
||||
// Handle unique (slot_id, date) violation gracefully
|
||||
if (isset($e->errorInfo[1]) && (int)$e->errorInfo[1] === 1062) {
|
||||
throw \Illuminate\Validation\ValidationException::withMessages([
|
||||
'slot_id' => 'That slot has just been taken. Please choose another.',
|
||||
]);
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('admin.calendar')
|
||||
->with('success', 'Booking created successfully.');
|
||||
}
|
||||
|
||||
public function createBlock(\Illuminate\Http\Request $request)
|
||||
{
|
||||
// Needed at top of file:
|
||||
// use App\Models\Slot;
|
||||
// use Carbon\Carbon;
|
||||
|
||||
$data = $request->validate([
|
||||
'date' => ['required','date_format:Y-m-d'],
|
||||
'slot_id' => ['required','exists:slots,id'],
|
||||
]);
|
||||
|
||||
$slot = \App\Models\Slot::findOrFail($data['slot_id']);
|
||||
|
||||
// Prettify for the view (no Carbon in Blade)
|
||||
$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');
|
||||
|
||||
return view('bookings.block', [
|
||||
'slot' => $slot,
|
||||
'date' => $data['date'],
|
||||
'prettyDate' => $prettyDate,
|
||||
'prettyTime' => $prettyTime,
|
||||
'weekdayName' => $weekdayName,
|
||||
]);
|
||||
}
|
||||
|
||||
public function storeBlock(\Illuminate\Http\Request $request)
|
||||
{
|
||||
// Validation: block a specific slot on a specific date
|
||||
$data = $request->validate([
|
||||
'slot_id' => ['required','exists:slots,id'],
|
||||
'date' => ['required','date_format:Y-m-d'],
|
||||
'message' => ['nullable','string'], // optional reason/notes for the block
|
||||
]);
|
||||
|
||||
// Create a "blocked" booking for that slot/date
|
||||
try {
|
||||
\App\Models\Booking::create([
|
||||
'slot_id' => (int)$data['slot_id'],
|
||||
'date' => $data['date'],
|
||||
'student_name' => null, // blocks don't need a student
|
||||
'booked_from_page' => 'admin:block', // provenance
|
||||
'message' => $data['message'] ?? null,
|
||||
'status' => 'blocked',
|
||||
]);
|
||||
} catch (\Illuminate\Database\QueryException $e) {
|
||||
// Unique (slot_id, date) hit — something already exists for that slot/date
|
||||
if (isset($e->errorInfo[1]) && (int)$e->errorInfo[1] === 1062) {
|
||||
return back()
|
||||
->withErrors(['slot_id' => 'That slot/date already has a booking or block.'])
|
||||
->withInput();
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('admin.calendar')
|
||||
->with('success', 'Slot blocked successfully.');
|
||||
}
|
||||
|
||||
public function createSeries(\Illuminate\Http\Request $request)
|
||||
{
|
||||
// Needed at top of file (ensure these exist):
|
||||
// use App\Models\Slot;
|
||||
// use Carbon\Carbon;
|
||||
|
||||
// Accept prefill from calendar click: ?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']);
|
||||
|
||||
// Precompute nice 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; // sensible default
|
||||
|
||||
return view('bookings.series', [
|
||||
'slot' => $slot,
|
||||
'date' => $data['date'],
|
||||
'prettyDate' => $prettyDate,
|
||||
'prettyTime' => $prettyTime,
|
||||
'weekdayName' => $weekdayName,
|
||||
'weeks' => $weeks,
|
||||
]);
|
||||
}
|
||||
|
||||
public function storeSeries(\Illuminate\Http\Request $request)
|
||||
{
|
||||
// Needed at top of file:
|
||||
// use App\Models\Slot;
|
||||
// use App\Models\Booking;
|
||||
// use Carbon\Carbon;
|
||||
// use Illuminate\Database\QueryException;
|
||||
|
||||
$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');
|
||||
|
||||
$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');
|
||||
|
||||
// Skip past or ≤ 2 hours from now
|
||||
$minutesUntil = $now->diffInMinutes($sessionStart, false);
|
||||
if ($minutesUntil <= 120) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
\App\Models\Booking::create([
|
||||
'slot_id' => $slot->id,
|
||||
'date' => $date,
|
||||
'student_name' => null, // series = no student name
|
||||
'booked_from_page' => 'admin:block-series', // provenance
|
||||
'message' => null,
|
||||
'status' => 'booked', // shows as “Booked” on the calendar
|
||||
]);
|
||||
$created++;
|
||||
} catch (\Illuminate\Database\QueryException $e) {
|
||||
// Skip duplicates (unique (slot_id, date) might already exist as booked/closed)
|
||||
if (isset($e->errorInfo[1]) && (int)$e->errorInfo[1] === 1062) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect back to the week containing the start date (nice UX)
|
||||
$displayStart = $now->copy()->startOfWeek(\Carbon\Carbon::MONDAY);
|
||||
$fridayCutoff = $displayStart->copy()->addDays(4)->setTime(15, 0);
|
||||
if ($now->greaterThanOrEqualTo($fridayCutoff)) {
|
||||
$displayStart->addWeek();
|
||||
}
|
||||
$weekOffset = $displayStart->diffInWeeks($startDate->copy()->startOfWeek(\Carbon\Carbon::MONDAY), false);
|
||||
|
||||
return redirect()
|
||||
->route('admin.calendar', ['week' => $weekOffset ?: null])
|
||||
->with('success', "Series created: {$created} added, {$skipped} skipped.");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
// app/Http/Controllers/Admin/CalendarController.php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\CalendarService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CalendarController extends Controller
|
||||
{
|
||||
public function index(\Illuminate\Http\Request $request)
|
||||
{
|
||||
// Make sure you also have at the top of the file:
|
||||
// use App\Models\Slot;
|
||||
// use Carbon\Carbon;
|
||||
|
||||
$now = \Carbon\Carbon::now('Europe/London');
|
||||
|
||||
// Default week, flip to next after Fri 15:00
|
||||
$start = $now->copy()->startOfWeek(\Carbon\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 with bookings for the visible week
|
||||
$slots = \App\Models\Slot::with(['bookings' => function ($q) use ($start, $end) {
|
||||
$q->whereBetween('date', [$start->toDateString(), $end->toDateString()]);
|
||||
}])->get();
|
||||
|
||||
// 👉 Pass $slots (and the other vars) to the view
|
||||
return view('calendar.index', compact('start', 'end', 'weekOffset', 'slots', 'now'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Course;
|
||||
use App\Models\Chapter;
|
||||
|
||||
class ChapterController extends Controller
|
||||
{
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'lesson_section_id' => 'required|exists:lesson_sections,id',
|
||||
'title' => 'required|max:255',
|
||||
]);
|
||||
|
||||
// Find the current max order for this section
|
||||
$maxOrder = Chapter::where('lesson_section_id', $request->lesson_section_id)->max('order');
|
||||
|
||||
// If no chapters yet, maxOrder will be null → start at 1
|
||||
$nextOrder = $maxOrder ? $maxOrder + 1 : 1;
|
||||
|
||||
Chapter::create([
|
||||
'lesson_section_id' => $request->lesson_section_id,
|
||||
'title' => $request->title,
|
||||
'order' => $nextOrder,
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Chapter added.');
|
||||
}
|
||||
|
||||
public function destroy(Chapter $chapter)
|
||||
{
|
||||
$chapter->delete();
|
||||
return back()->with('success', 'Chapter removed.');
|
||||
}
|
||||
|
||||
public function reorder(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'order' => 'required|string',
|
||||
]);
|
||||
|
||||
$ids = json_decode($request->order, true);
|
||||
|
||||
if (is_array($ids)) {
|
||||
foreach ($ids as $index => $id) {
|
||||
Chapter::where('id', $id)->update(['order' => $index + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
return back()->with('success', 'Chapter order updated.');
|
||||
}
|
||||
|
||||
public function show(Course $course, Chapter $chapter)
|
||||
{
|
||||
// (Optional) safety check: make sure chapter belongs to this course
|
||||
// via its module / section chain, if you want to enforce that later.
|
||||
return view('admin.chapters.show', [
|
||||
'course' => $course,
|
||||
'chapter' => $chapter,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Course;
|
||||
use App\Models\Subject;
|
||||
use App\Models\Level;
|
||||
|
||||
class CourseController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$courses = Course::all();
|
||||
return view('admin.courses.index', compact('courses'));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$subjects = \App\Models\Subject::all();
|
||||
$levels = \App\Models\Level::all();
|
||||
|
||||
return view('admin.courses.create', compact('subjects', 'levels'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'subject_id' => 'required|exists:subjects,id',
|
||||
'level_id' => 'required|exists:levels,id',
|
||||
'lead_teacher' => 'required|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
]);
|
||||
|
||||
Course::create([
|
||||
'subject_id' => $request->input('subject_id'),
|
||||
'level_id' => $request->input('level_id'),
|
||||
'lead_teacher' => $request->input('lead_teacher'),
|
||||
'description' => $request->input('description'),
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.courses.index')
|
||||
->with('success', 'Course added successfully!');
|
||||
}
|
||||
|
||||
public function show(Course $course)
|
||||
{
|
||||
// Eager load related modules/sections/chapters so they’re available
|
||||
$course->load([
|
||||
'modules.lessonSections.chapters'
|
||||
]);
|
||||
|
||||
return view('admin.courses.show', compact('course'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the admin dashboard.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
// You can pass data here later (e.g., user stats, recent activity)
|
||||
return view('admin.dashboard');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Models\Course;
|
||||
use App\Models\Lesson;
|
||||
use App\Models\LessonVideo;
|
||||
use App\Models\Chapter;
|
||||
|
||||
class LessonController extends Controller
|
||||
{
|
||||
public function reorder(Request $request)
|
||||
{
|
||||
$ids = json_decode($request->order, true);
|
||||
|
||||
if (!is_array($ids)) {
|
||||
return back()->with('error', 'Invalid order data.');
|
||||
}
|
||||
|
||||
foreach ($ids as $index => $id) {
|
||||
Lesson::where('id', $id)->update(['order' => $index + 1]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Lesson order updated.');
|
||||
}
|
||||
|
||||
public function show($courseId, $chapterId, Lesson $lesson)
|
||||
{
|
||||
// later we’ll validate that the lesson belongs to this chapter & course
|
||||
return view('admin.lessons.show', compact('lesson', 'chapterId', 'courseId'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'chapter_id' => 'required|exists:chapters,id',
|
||||
'title' => 'required|string|max:255',
|
||||
]);
|
||||
|
||||
$order = Lesson::where('chapter_id', $request->chapter_id)->max('order') + 1;
|
||||
|
||||
Lesson::create([
|
||||
'chapter_id' => $request->chapter_id,
|
||||
'title' => $request->title,
|
||||
'slug' => Str::slug($request->title),
|
||||
'description' => $request->description ?? null,
|
||||
'order' => $order,
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Lesson added.');
|
||||
}
|
||||
|
||||
public function destroy(Course $course, Chapter $chapter, Lesson $lesson)
|
||||
{
|
||||
$lesson->delete();
|
||||
|
||||
return redirect()->route('admin.chapters.show', [
|
||||
'course' => $course->slug,
|
||||
'chapter' => $chapter->slug,
|
||||
])->with('success', 'Lesson deleted.');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Course;
|
||||
use App\Models\LessonSection;
|
||||
|
||||
class LessonSectionController extends Controller
|
||||
{
|
||||
|
||||
public function show(Course $course, LessonSection $section)
|
||||
{
|
||||
// Optional safety: ensure this section actually belongs to this course
|
||||
// (assuming LessonSection -> module -> course)
|
||||
if ($section->module && $section->module->course_id !== $course->id) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
// Eager load chapters for this section
|
||||
$section->load(['chapters' => function ($q) {
|
||||
$q->orderBy('order');
|
||||
}]);
|
||||
|
||||
return view('admin.sections.show', [
|
||||
'course' => $course,
|
||||
'section' => $section,
|
||||
]);
|
||||
}
|
||||
|
||||
public function index(Course $course)
|
||||
{
|
||||
$sections = $course->lessonSections()->orderBy('order')->get();
|
||||
|
||||
return view('admin.lesson_sections.index', compact('course', 'sections'));
|
||||
}
|
||||
|
||||
public function store(Request $request, Course $course)
|
||||
{
|
||||
$request->validate([
|
||||
'module_id' => 'required|exists:modules,id',
|
||||
'title' => 'required|string|max:255',
|
||||
'year' => 'nullable|integer',
|
||||
]);
|
||||
|
||||
$module = \App\Models\Module::findOrFail($request->module_id);
|
||||
|
||||
// Safety check: ensure module belongs to this course
|
||||
if ($module->course_id !== $course->id) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
LessonSection::create([
|
||||
'module_id' => $module->id,
|
||||
'title' => $request->title,
|
||||
'year' => $request->year,
|
||||
'order' => $module->lessonSections()->count() + 1,
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Lesson section added.');
|
||||
}
|
||||
|
||||
public function update(Request $request, Course $course, LessonSection $lessonSection)
|
||||
{
|
||||
$request->validate([
|
||||
'title' => 'required|string|max:255',
|
||||
'year' => 'nullable|integer',
|
||||
]);
|
||||
|
||||
$lessonSection->update([
|
||||
'title' => $request->title,
|
||||
'year' => $request->year,
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Lesson section updated.');
|
||||
}
|
||||
|
||||
public function destroy(Course $course, LessonSection $section)
|
||||
{
|
||||
// Ensure the section belongs to the course via the module
|
||||
if ($section->module->course_id !== $course->id) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
// Delete all chapters and their lessons/videos
|
||||
foreach ($section->chapters as $chapter) {
|
||||
// delete lessons inside the chapter
|
||||
foreach ($chapter->lessons as $lesson) {
|
||||
$lesson->videos()->delete();
|
||||
}
|
||||
|
||||
$chapter->lessons()->delete();
|
||||
$chapter->delete();
|
||||
}
|
||||
|
||||
// Delete the section itself
|
||||
$section->delete();
|
||||
|
||||
return redirect()
|
||||
->route('admin.courses.show', $course->slug)
|
||||
->with('success', 'Lesson section deleted.');
|
||||
}
|
||||
|
||||
public function reorder(Request $request, Course $course)
|
||||
{
|
||||
$ids = json_decode($request->order, true);
|
||||
|
||||
foreach ($ids as $i => $id) {
|
||||
LessonSection::where('id', $id)->update(['order' => $i + 1]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Lesson section order updated.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\LessonVideo;
|
||||
|
||||
class LessonVideoController extends Controller
|
||||
{
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'lesson_id' => 'required|exists:lessons,id',
|
||||
'title' => 'required|max:255',
|
||||
'video_url' => 'required|url',
|
||||
]);
|
||||
|
||||
$maxOrder = LessonVideo::where('lesson_id', $request->lesson_id)->max('order');
|
||||
$nextOrder = $maxOrder ? $maxOrder + 1 : 1;
|
||||
|
||||
LessonVideo::create([
|
||||
'lesson_id' => $request->lesson_id,
|
||||
'title' => $request->title,
|
||||
'video_url' => $request->video_url,
|
||||
'order' => $nextOrder,
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Video added.');
|
||||
}
|
||||
|
||||
public function reorder(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'order' => 'required|string',
|
||||
]);
|
||||
|
||||
$ids = json_decode($request->order, true);
|
||||
|
||||
if (is_array($ids)) {
|
||||
foreach ($ids as $index => $id) {
|
||||
LessonVideo::where('id', $id)->update(['order' => $index + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
return back()->with('success', 'Video order updated.');
|
||||
}
|
||||
|
||||
public function destroy(LessonVideo $video)
|
||||
{
|
||||
$video->delete();
|
||||
return back()->with('success', 'Video removed.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Course;
|
||||
use App\Models\Module;
|
||||
|
||||
class ModuleController extends Controller
|
||||
{
|
||||
public function create()
|
||||
{
|
||||
$courses = Course::with(['subject', 'level'])->get();
|
||||
return view('admin.modules.create', compact('courses'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'course_id' => 'required|exists:courses,id',
|
||||
'name' => 'required|string|max:255',
|
||||
]);
|
||||
|
||||
$course = Course::findOrFail($request->course_id);
|
||||
|
||||
$course->modules()->create([
|
||||
'name' => $request->name,
|
||||
'order' => $course->modules()->count() + 1,
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.courses.show', $course->slug)
|
||||
->with('success', 'Module added.');
|
||||
}
|
||||
|
||||
public function reorder(Request $request)
|
||||
{
|
||||
$ids = json_decode($request->order, true);
|
||||
|
||||
foreach ($ids as $index => $id) {
|
||||
Module::where('id', $id)->update(['order' => $index + 1]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Module order updated.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ParentProfile;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ParentController extends Controller
|
||||
{
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$q = $request->string('q')->toString();
|
||||
|
||||
$parents = ParentProfile::query()
|
||||
->with('user')
|
||||
->when($q, function ($query) use ($q) {
|
||||
$query->whereHas('user', function ($u) use ($q) {
|
||||
$u->where('first_name', 'like', "%{$q}%")
|
||||
->orWhere('last_name', 'like', "%{$q}%")
|
||||
->orWhere('email', 'like', "%{$q}%");
|
||||
});
|
||||
})
|
||||
->orderByDesc('created_at')
|
||||
->paginate(25)
|
||||
->withQueryString();
|
||||
|
||||
return view('admin.parents.index', compact('parents', 'q'));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$parents = ParentProfile::with('user')->get();
|
||||
$studentTypes = StudentType::all();
|
||||
|
||||
return view('admin.students.create', compact('parents', 'studentTypes'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['nullable', 'email', 'max:255'],
|
||||
'phone' => ['nullable', 'string', 'max:50'],
|
||||
'notes' => ['nullable', 'string'],
|
||||
]);
|
||||
|
||||
ParentProfile::create($validated);
|
||||
|
||||
return redirect()->route('admin.parents.index')
|
||||
->with('success', 'Parent created.');
|
||||
}
|
||||
|
||||
public function edit(ParentProfile $parent)
|
||||
{
|
||||
return view('admin.parents.edit', compact('parent'));
|
||||
}
|
||||
|
||||
public function update(Request $request, ParentProfile $parent)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['nullable', 'email', 'max:255'],
|
||||
'phone' => ['nullable', 'string', 'max:50'],
|
||||
'notes' => ['nullable', 'string'],
|
||||
]);
|
||||
|
||||
$parent->update($validated);
|
||||
|
||||
return redirect()->route('admin.parents.edit', $parent)
|
||||
->with('success', 'Parent updated.');
|
||||
}
|
||||
|
||||
public function destroy(ParentProfile $parent)
|
||||
{
|
||||
$parent->delete();
|
||||
|
||||
return redirect()->route('admin.parents.index')
|
||||
->with('success', 'Parent deleted.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Models\StudentType;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use App\Models\StudentProfile;
|
||||
use App\Models\BillingAccount;
|
||||
use App\Models\ParentProfile;
|
||||
|
||||
class StudentController extends Controller
|
||||
{
|
||||
|
||||
|
||||
|
||||
public function index()
|
||||
{
|
||||
$students = StudentProfile::with(['user', 'billingAccount'])->get();
|
||||
|
||||
return view('admin.students.index', compact('students'));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$parents = ParentProfile::with('user')->get();
|
||||
$studentTypes = StudentType::all();
|
||||
|
||||
return view('admin.students.create', compact('parents', 'studentTypes'));
|
||||
}
|
||||
|
||||
public function edit(StudentProfile $student)
|
||||
{
|
||||
$student->load(['user', 'billingAccount', 'billingAccount.primaryParent']);
|
||||
|
||||
$parents = ParentProfile::with('user')->get();
|
||||
$studentTypes = StudentType::all();
|
||||
|
||||
return view('admin.students.edit', compact('student', 'parents', 'studentTypes'));
|
||||
}
|
||||
|
||||
public function update(Request $request, StudentProfile $student)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
// Student
|
||||
'student_first_name' => 'required|string|max:255',
|
||||
'student_last_name' => 'required|string|max:255',
|
||||
'student_phone' => 'nullable|string|max:255',
|
||||
// If you allow changing student email:
|
||||
// 'student_email' => ['required','email', Rule::unique('users','email')->ignore($student->user_id)],
|
||||
|
||||
'date_of_birth' => 'nullable|date',
|
||||
'student_type_id' => 'required|exists:student_types,id',
|
||||
|
||||
// Parent selection
|
||||
'parent_option' => 'required|in:none,existing,new',
|
||||
'existing_parent_id' => 'required_if:parent_option,existing|nullable|exists:parent_profiles,id',
|
||||
|
||||
// New parent (only used if parent_option=new)
|
||||
'parent_first_name' => 'required_if:parent_option,new|nullable|string|max:255',
|
||||
'parent_last_name' => 'required_if:parent_option,new|nullable|string|max:255',
|
||||
'parent_email' => 'required_if:parent_option,new|nullable|email|unique:users,email',
|
||||
'parent_phone' => 'nullable|string|max:255',
|
||||
|
||||
// Billing (match your edit form; keep minimal initially)
|
||||
'billing_account_id' => 'sometimes|nullable|exists:billing_accounts,id',
|
||||
'billing_name' => 'required|string|max:255',
|
||||
'invoice_email' => 'required|email',
|
||||
'billing_phone' => 'nullable|string|max:255',
|
||||
'contact_name' => 'nullable|string|max:255',
|
||||
'address_line1' => 'required|string|max:255',
|
||||
'address_line2' => 'nullable|string|max:255',
|
||||
'town' => 'required|string|max:255',
|
||||
'postcode' => 'required|string|max:50',
|
||||
'country' => 'required|string|max:255',
|
||||
]);
|
||||
|
||||
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
// Load student user
|
||||
$student->load('user');
|
||||
|
||||
// 1) Update STUDENT USER
|
||||
$student->user->update([
|
||||
'first_name' => $validated['student_first_name'],
|
||||
'last_name' => $validated['student_last_name'],
|
||||
'phone' => $validated['student_phone'] ?? null,
|
||||
// 'email' => $validated['student_email'],
|
||||
]);
|
||||
|
||||
// 2) Update STUDENT PROFILE
|
||||
$student->update([
|
||||
'date_of_birth' => $validated['date_of_birth'] ?? null,
|
||||
'student_type_id' => $validated['student_type_id'],
|
||||
// billing_account_id handled below
|
||||
]);
|
||||
|
||||
// 3) Resolve parent user based on parent_option
|
||||
$parentProfile = null;
|
||||
|
||||
if ($validated['parent_option'] === 'existing') {
|
||||
$parentProfile = ParentProfile::with('user')->find($validated['existing_parent_id']);
|
||||
}
|
||||
|
||||
if ($validated['parent_option'] === 'new') {
|
||||
$parentUser = User::create([
|
||||
'first_name' => $validated['parent_first_name'],
|
||||
'last_name' => $validated['parent_last_name'],
|
||||
'email' => $validated['parent_email'],
|
||||
'phone' => $validated['parent_phone'],
|
||||
'password' => Hash::make(str()->random(16)),
|
||||
]);
|
||||
|
||||
$parentUser->assignRole('parent');
|
||||
|
||||
$parentProfile = ParentProfile::firstOrCreate(
|
||||
['user_id' => $parentUser->id],
|
||||
['phone' => $parentUser->phone]
|
||||
);
|
||||
|
||||
// ensure $parentProfile has user loaded for later
|
||||
$parentProfile->load('user');
|
||||
}
|
||||
|
||||
if ($parentProfile && $parentProfile->user) {
|
||||
// ensure role even for the "existing" case
|
||||
$parentProfile->user->assignRole('parent');
|
||||
}
|
||||
|
||||
// 4) Billing account: update existing or create new (same logic as store)
|
||||
if (!empty($validated['billing_account_id'])) {
|
||||
$billingAccount = BillingAccount::findOrFail($validated['billing_account_id']);
|
||||
$billingAccount->update([
|
||||
'name' => $validated['billing_name'],
|
||||
'contact_name' => $validated['contact_name'],
|
||||
'invoice_email' => $validated['invoice_email'],
|
||||
'phone' => $validated['billing_phone'],
|
||||
'address_line1' => $validated['address_line1'],
|
||||
'address_line2' => $validated['address_line2'],
|
||||
'town' => $validated['town'],
|
||||
'postcode' => $validated['postcode'],
|
||||
'country' => $validated['country'],
|
||||
'primary_parent_user_id' => $parentProfile?->user_id,
|
||||
]);
|
||||
} else {
|
||||
$billingAccount = BillingAccount::create([
|
||||
'name' => $validated['billing_name'],
|
||||
'contact_name' => $validated['contact_name'],
|
||||
'invoice_email' => $validated['invoice_email'],
|
||||
'phone' => $validated['billing_phone'],
|
||||
'address_line1' => $validated['address_line1'],
|
||||
'address_line2' => $validated['address_line2'],
|
||||
'town' => $validated['town'],
|
||||
'postcode' => $validated['postcode'],
|
||||
'country' => $validated['country'],
|
||||
'primary_parent_user_id' => $parentProfile?->user_id,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($validated['parent_option'] === 'none') {
|
||||
$billingAccount->update(['primary_parent_user_id' => null]);
|
||||
}
|
||||
|
||||
// Ensure student profile points at the billing account we just resolved
|
||||
$student->billing_account_id = $billingAccount->id;
|
||||
$student->save();
|
||||
|
||||
// 5) Parent pivot handling
|
||||
if ($validated['parent_option'] === 'none') {
|
||||
$student->parents()->detach();
|
||||
} else {
|
||||
if ($parentProfile) {
|
||||
// ONE parent only:
|
||||
$student->parents()->sync([
|
||||
$parentProfile->id => [
|
||||
'relationship' => null,
|
||||
'notes' => null,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
return redirect()
|
||||
->route('admin.students.edit', $student->id)
|
||||
->with('success', 'Student updated successfully.');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
// -----------------------------
|
||||
// Validation
|
||||
// -----------------------------
|
||||
$validated = $request->validate([
|
||||
// Student
|
||||
'student_first_name' => 'required|string|max:255',
|
||||
'student_last_name' => 'required|string|max:255',
|
||||
'student_email' => 'required|email|unique:users,email',
|
||||
'student_phone' => 'nullable|string|max:255',
|
||||
'date_of_birth' => 'nullable|date',
|
||||
'student_type_id' => 'required|exists:student_types,id',
|
||||
|
||||
// Parent selection
|
||||
'parent_option' => 'required|in:none,existing,new',
|
||||
'existing_parent_id' => 'required_if:parent_option,existing|nullable|exists:parent_profiles,id',
|
||||
|
||||
// New parent
|
||||
'parent_first_name' => 'nullable|string|max:255',
|
||||
'parent_last_name' => 'nullable|string|max:255',
|
||||
'parent_email' => 'nullable|email|unique:users,email',
|
||||
'parent_phone' => 'nullable|string|max:255',
|
||||
|
||||
// Billing
|
||||
'billing_account_id' => 'sometimes|nullable|exists:billing_accounts,id',
|
||||
'billing_name' => 'required|string|max:255',
|
||||
'invoice_email' => 'required|email',
|
||||
'billing_phone' => 'nullable|string|max:255',
|
||||
'contact_name' => 'nullable|string|max:255',
|
||||
'address_line1' => 'required|string|max:255',
|
||||
'address_line2' => 'nullable|string|max:255',
|
||||
'town' => 'required|string|max:255',
|
||||
'postcode' => 'required|string|max:50',
|
||||
'country' => 'required|string|max:255',
|
||||
]);
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
|
||||
// ===============================================================
|
||||
// 1) Create STUDENT USER
|
||||
// ===============================================================
|
||||
$studentUser = User::create([
|
||||
'first_name' => $validated['student_first_name'],
|
||||
'last_name' => $validated['student_last_name'],
|
||||
'email' => $validated['student_email'],
|
||||
'phone' => $validated['student_phone'],
|
||||
'password' => Hash::make(str()->random(16)), // Temporary password
|
||||
]);
|
||||
|
||||
$studentUser->assignRole('student');
|
||||
|
||||
|
||||
// ===============================================================
|
||||
// 2) Handle PARENT selection
|
||||
// ===============================================================
|
||||
|
||||
$parentProfile = null;
|
||||
|
||||
if ($validated['parent_option'] === 'existing') {
|
||||
$parentProfile = ParentProfile::find($validated['existing_parent_id']);
|
||||
}
|
||||
|
||||
if ($validated['parent_option'] === 'new') {
|
||||
create([
|
||||
'first_name' => $validated['parent_first_name'],
|
||||
'last_name' => $validated['parent_last_name'],
|
||||
'email' => $validated['parent_email'],
|
||||
'phone' => $validated['parent_phone'],
|
||||
'password' => Hash::make(str()->random(16)),
|
||||
]);
|
||||
$parentUser->assignRole('parent');
|
||||
|
||||
$parentProfile = ParentProfile::firstOrCreate(
|
||||
['user_id' => $parentUser->id],
|
||||
['phone' => $parentUser->phone]
|
||||
);
|
||||
}
|
||||
|
||||
if ($parentProfile) {
|
||||
$parentProfile->user?->assignRole('parent');
|
||||
}
|
||||
|
||||
// Note: If parent_option == 'none', $parentUser remains null.
|
||||
|
||||
|
||||
// ===============================================================
|
||||
// 3) Create BILLING ACCOUNT
|
||||
// ===============================================================
|
||||
if (!empty($validated['billing_account_id'])) {
|
||||
// Use existing billing account
|
||||
$billingAccount = BillingAccount::find($validated['billing_account_id']);
|
||||
} else {
|
||||
$billingAccount = BillingAccount::create([
|
||||
'name' => $validated['billing_name'],
|
||||
'contact_name' => $validated['contact_name'],
|
||||
'invoice_email' => $validated['invoice_email'],
|
||||
'phone' => $validated['billing_phone'],
|
||||
'address_line1' => $validated['address_line1'],
|
||||
'address_line2' => $validated['address_line2'],
|
||||
'town' => $validated['town'],
|
||||
'postcode' => $validated['postcode'],
|
||||
'country' => $validated['country'],
|
||||
'primary_parent_user_id' => $parentProfile?->user_id,
|
||||
]);
|
||||
}
|
||||
|
||||
// ===============================================================
|
||||
// 4) Create STUDENT PROFILE
|
||||
// ===============================================================
|
||||
$studentProfile = StudentProfile::create([
|
||||
'user_id' => $studentUser->id,
|
||||
'date_of_birth' => $validated['date_of_birth'] ?? null,
|
||||
'billing_account_id' => $billingAccount->id,
|
||||
'student_type_id' => $validated['student_type_id'],
|
||||
]);
|
||||
|
||||
// ===============================================================
|
||||
// 4B) Create/link PARENT PROFILE + pivot relationship
|
||||
// ===============================================================
|
||||
if ($parentUser) {
|
||||
|
||||
// Ensure parent has role (covers "existing parent" case)
|
||||
$parentUser->assignRole('parent');
|
||||
|
||||
// Ensure parent profile exists
|
||||
$parentProfile = ParentProfile::firstOrCreate(
|
||||
['user_id' => $parentUser->id],
|
||||
['phone' => $parentUser->phone] // optional default
|
||||
);
|
||||
|
||||
// Link parent profile to student profile via pivot
|
||||
$studentProfile->parents()->syncWithoutDetaching([
|
||||
$parentProfile->id => [
|
||||
'relationship' => null,
|
||||
'notes' => null,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
// ===============================================================
|
||||
// 5) Send password setup email to student user
|
||||
// ===============================================================
|
||||
Password::sendResetLink(['email' => $studentUser->email]);
|
||||
|
||||
|
||||
DB::commit();
|
||||
|
||||
return redirect()
|
||||
->route('admin.students.index')
|
||||
->with('success', 'Student created successfully.');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
throw $e; // (You can customise error messages later)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use App\Models\User;
|
||||
use App\Models\TeacherProfile;
|
||||
use App\Models\Subject;
|
||||
use App\Models\Level;
|
||||
|
||||
class TeacherController extends Controller
|
||||
{
|
||||
|
||||
public function index()
|
||||
{
|
||||
$teachers = \App\Models\TeacherProfile::with('user')->get();
|
||||
|
||||
return view('admin.teachers.index', compact('teachers'));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$subjects = Subject::orderBy('name')->get();
|
||||
$levels = Level::orderBy('name')->get();
|
||||
|
||||
return view('admin.teachers.create', compact('subjects', 'levels'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
// Later we’ll add validation here
|
||||
|
||||
// Create the User
|
||||
$user = User::create([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'password' => Hash::make($request->password),
|
||||
]);
|
||||
|
||||
$user->assignRole('teacher');
|
||||
|
||||
// Handle profile picture upload (placeholder for now)
|
||||
$pathToFile = null;
|
||||
if ($request->hasFile('picture')) {
|
||||
$pathToFile = $request->file('picture')->store('teacher_pictures', 'public');
|
||||
}
|
||||
|
||||
// Create the TeacherProfile
|
||||
$teacher = TeacherProfile::create([
|
||||
'user_id' => $user->id,
|
||||
'bio' => $request->bio,
|
||||
'location' => $request->location,
|
||||
'hourly_rate'=> $request->hourly_rate,
|
||||
'picture' => $pathToFile,
|
||||
]);
|
||||
|
||||
// For later: attach subjects and levels
|
||||
// $teacher->subjects()->sync($request->subjects);
|
||||
// $teacher->levels()->sync($request->levels);
|
||||
|
||||
// Redirect somewhere (dashboard for now)
|
||||
return redirect()->route('admin.dashboard')
|
||||
->with('success', 'Teacher added successfully!');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$users = User::with('roles')->get();
|
||||
$roles = Role::all();
|
||||
|
||||
return view('admin.users.index', compact('users', 'roles'));
|
||||
}
|
||||
|
||||
public function updateRole(Request $request, User $user)
|
||||
{
|
||||
$request->validate([
|
||||
'role' => 'required|exists:roles,name',
|
||||
]);
|
||||
|
||||
$user->syncRoles([$request->role]);
|
||||
|
||||
return redirect()->back()->with('success', "{$user->name} is now a {$request->role}.");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user